From 859cabd07d5ab8b441db114cecb299cffcb30e26 Mon Sep 17 00:00:00 2001 From: anshul Date: Wed, 14 Oct 2026 10:28:09 +0530 Subject: [PATCH] feat(investment): settle purchases to post them to GL on the settlement date Settlement-date accounting (RBI UCB Investment Directions, confirmed by Jitendra Sir): a saved purchase is PENDING_SETTLEMENT with nothing in GL. On or after its settlement date, POST /investment/purchases/{id}/settle posts the journal entry dated the settlement date at Head Office - Dr investment GL (book value), Dr 500606 broken-period interest, Dr charge GLs, Cr the paying bank - through the journal entry command, which also raises the JV voucher. The deal becomes ACTIVE and keeps the GL transaction id and settlement day. Settle goes through maker-checker; a settled deal can no longer be dropped. Broken-period interest is now counted up to the settlement date instead of the deal date, and the interest schedule starts from it. Migration 0597 adds the settlement columns and the SETTLE permission. Co-Authored-By: Claude Opus 5.5 --- .../service/CommandWrapperBuilder.java | 8 ++ .../api/InvestmentPurchaseApiConstants.java | 9 ++ .../api/InvestmentPurchaseApiResource.java | 13 +++ .../data/InvestmentPurchaseData.java | 5 +- .../investment/domain/InvestmentPurchase.java | 7 ++ ...ettleInvestmentPurchaseCommandHandler.java | 42 ++++++++ .../service/InvestmentInterestCalculator.java | 2 +- ...InvestmentPurchaseReadPlatformService.java | 2 +- ...nvestmentPurchaseWritePlatformService.java | 2 + ...tmentPurchaseWritePlatformServiceImpl.java | 95 ++++++++++++++++++- .../tenant/final-changelog-tenant.xml | 1 + .../0597_investment_purchase_settlement.xml | 74 +++++++++++++++ 12 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/SettleInvestmentPurchaseCommandHandler.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_investment_purchase_settlement.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index d4c0153be..39c7f3e9d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -151,6 +151,14 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder settleInvestmentPurchase(final Long purchaseId) { + this.actionName = "SETTLE"; + this.entityName = "INVESTMENTPURCHASE"; + this.entityId = purchaseId; + this.href = "/investment/purchases/" + purchaseId + "/settle"; + return this; + } + public CommandWrapperBuilder createInvestmentDailyRate() { this.actionName = "CREATE"; this.entityName = "INVESTMENTDAILYRATE"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java index c6bb013d8..2742d4a4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java @@ -31,8 +31,17 @@ public final class InvestmentPurchaseApiConstants { public static final Set SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2"); public static final Set CATEGORIES = Set.of("HTM (Held To Maturity)", "AFS (Available For Sale)", "HFT (Held For Trading)"); + /** Saved deal; nothing posted to GL until it is settled. */ + public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT"; + /** Settled: posted to GL on the settlement date, now a holding. */ public static final String STATUS_ACTIVE = "ACTIVE"; + /** GLs the settlement entry debits besides the script's investment GL. */ + public static final String BROKEN_PERIOD_INTEREST_GL_CODE = "500606"; + public static final String STAMP_DUTY_GL_CODE = "500601"; + public static final String BROKERAGE_GL_CODE = "500602"; + public static final String OTHER_CHARGES_GL_CODE = "500603"; + public static final String BOND_TYPE = "bondType"; public static final String SCRIPT_ID = "scriptId"; public static final String PARTY_ID = "partyId"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java index 5aebaf4f6..790949c67 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java @@ -95,6 +95,19 @@ public class InvestmentPurchaseApiResource { return this.commandsSourceWritePlatformService.logCommandSource(request); } + @POST + @Path("{purchaseId}/settle") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Settle a purchase", description = "On or after the settlement date: posts the purchase to GL dated the " + + "settlement date and makes the deal an active holding.") + public CommandProcessingResult settle(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentPurchase(purchaseId) + .withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } + @DELETE @Path("{purchaseId}") @Produces({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java index 75129d93e..01c8bd328 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java @@ -73,6 +73,8 @@ public class InvestmentPurchaseData { private final String crGlName; private final String otherDetails; private final String status; + private final String glTransactionId; + private final String settledOnDate; public static InvestmentPurchaseData from(final InvestmentPurchase p) { return InvestmentPurchaseData.builder().id(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo()).bondType(p.getBondType()) @@ -88,7 +90,8 @@ public class InvestmentPurchaseData { .subType(p.getSubType()).investmentGlAccountId(p.getInvestmentGlAccount().getId()) .investmentGlCode(p.getInvestmentGlAccount().getGlCode()).investmentGlName(p.getInvestmentGlAccount().getName()) .crGlAccountId(p.getCrGlAccount().getId()).crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName()) - .otherDetails(p.getOtherDetails()).status(p.getStatus()).build(); + .otherDetails(p.getOtherDetails()).status(p.getStatus()).glTransactionId(p.getGlTransactionId()) + .settledOnDate(format(p.getSettledOnDate())).build(); } private static String format(final LocalDate date) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java index 38858fa29..a65469f39 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java @@ -148,4 +148,11 @@ public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom schedule(final BigDecimal faceValue, final BigDecimal ratePercent, final LocalDate maturity, final LocalDate lastIntDate, final LocalDate dealDate) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java index fc63727ac..e5848d999 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java @@ -66,7 +66,7 @@ public class InvestmentPurchaseReadPlatformService { final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate(); final List rows = InvestmentInterestCalculator - .schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate()).stream() + .schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getSettlementDate()).stream() .map(r -> InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date())) .fromDate(format(r.fromDate())).toDate(format(r.toDate())).days(r.days()).grossInterest(r.grossInterest()) .alreadyBooked(r.alreadyBooked()).amount(r.amount()).due(!r.date().isAfter(asOn)).build()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java index 61d495d21..30ca1e95e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java @@ -26,4 +26,6 @@ public interface InvestmentPurchaseWritePlatformService { CommandProcessingResult createPurchase(JsonCommand command); CommandProcessingResult deletePurchase(Long purchaseId); + + CommandProcessingResult settlePurchase(Long purchaseId, JsonCommand command); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java index 9a557ebb9..796688810 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java @@ -19,7 +19,9 @@ package org.apache.fineract.portfolio.investment.service; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE; +import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKEN_PERIOD_INTEREST_GL_CODE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE; +import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE_GL_CODE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID; @@ -29,29 +31,39 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApi import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_CHARGES; +import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_CHARGES_GL_CODE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY; +import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY_GL_CODE; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE; +import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT; import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; +import java.util.Locale; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper; import org.apache.fineract.accounting.glaccount.domain.GLAccountType; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.investment.domain.InvestmentParty; import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository; import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase; @@ -82,6 +94,8 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur private final InvestmentPartyRepository partyRepository; private final GLAccountRepositoryWrapper glAccountRepository; private final JdbcTemplate jdbcTemplate; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final FromJsonHelper fromApiJsonHelper; @Override @Transactional @@ -130,7 +144,7 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur p.setInvestmentGlAccount(script.getGlAccount()); p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID))); p.setOtherDetails(otherDetails(command)); - p.setStatus(STATUS_ACTIVE); + p.setStatus(STATUS_PENDING_SETTLEMENT); // Brokerage, stamp duty and other charges are expenses of the deal (Jitendra Sir): paid with the // settlement, but kept out of the book value so the charges stay visible on their own GLs. @@ -154,6 +168,9 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur public CommandProcessingResult deletePurchase(final Long purchaseId) { final InvestmentPurchase p = this.purchaseRepository.findById(purchaseId) .orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId)); + if (!STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) { + throw rule("purchase.settled", "Deal " + p.getDealNo() + " is already settled and posted to GL, so it cannot be dropped"); + } if (p.getBalanceUnits().compareTo(p.getUnits()) != 0) { throw rule("purchase.partly.sold", "Deal " + p.getDealNo() + " has been partly sold and cannot be dropped"); } @@ -162,6 +179,78 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur return new CommandProcessingResultBuilder().withEntityId(purchaseId).withResourceIdAsString(p.getDealNo()).build(); } + /** + * Settles a deal: on or after its settlement date the money has gone and the security has come in, so the purchase + * is posted to GL dated the settlement date (settlement-date accounting) and the deal becomes a holding. Posted + * through the journal entry command like any manual entry, inside this transaction, so under maker-checker nothing + * reaches the GL until the settlement is approved. + */ + @Override + @Transactional + public CommandProcessingResult settlePurchase(final Long purchaseId, final JsonCommand command) { + final InvestmentPurchase p = this.purchaseRepository.findById(purchaseId) + .orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId)); + if (!STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) { + throw rule("settle.not.pending", "Deal " + p.getDealNo() + " is not pending settlement"); + } + final LocalDate today = DateUtils.getBusinessLocalDate(); + if (p.getSettlementDate().isAfter(today)) { + throw rule("settle.before.date", "Deal " + p.getDealNo() + " can be settled on or after its settlement date " + + p.getSettlementDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy"))); + } + + final JsonArray debits = new JsonArray(); + addLine(debits, p.getInvestmentGlAccount().getId(), p.getBookValue()); + addLine(debits, glId(BROKEN_PERIOD_INTEREST_GL_CODE), p.getAccruedInterest()); + addLine(debits, glId(STAMP_DUTY_GL_CODE), p.getStampDuty()); + addLine(debits, glId(BROKERAGE_GL_CODE), p.getBrokerage()); + addLine(debits, glId(OTHER_CHARGES_GL_CODE), p.getOtherCharges()); + final JsonArray credits = new JsonArray(); + addLine(credits, p.getCrGlAccount().getId(), p.getSettlementAmount()); + + final String dateFormat = "dd MMMM yyyy"; + final JsonObject payload = new JsonObject(); + payload.addProperty("officeId", headOfficeId()); + payload.addProperty("transactionDate", p.getSettlementDate().format(DateTimeFormatter.ofPattern(dateFormat, Locale.ENGLISH))); + payload.addProperty("dateFormat", dateFormat); + payload.addProperty("locale", "en"); + payload.addProperty("currencyCode", "INR"); + payload.addProperty("referenceNumber", p.getDealNo()); + payload.addProperty("comments", "Investment purchase settled: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " " + + p.getScript().getScriptDesc() + ", " + p.getParty().getPartyName()); + payload.add("debits", debits); + payload.add("credits", credits); + final String json = payload.toString(); + final JsonCommand journalEntryCommand = JsonCommand.from(json, this.fromApiJsonHelper.parse(json), this.fromApiJsonHelper, null, + null, null, null, null, null, null, null, null, null, null, null, null, null); + final CommandProcessingResult posted = this.journalEntryWritePlatformService.createJournalEntry(journalEntryCommand); + + p.setGlTransactionId(posted.getTransactionId()); + p.setSettledOnDate(today); + p.setStatus(STATUS_ACTIVE); + this.purchaseRepository.saveAndFlush(p); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(purchaseId) + .withResourceIdAsString(p.getDealNo()).withTransactionId(posted.getTransactionId()).build(); + } + + private static void addLine(final JsonArray lines, final Long glAccountId, final BigDecimal amount) { + if (amount != null && amount.signum() > 0) { + final JsonObject line = new JsonObject(); + line.addProperty("glAccountId", glAccountId); + line.addProperty("amount", amount); + lines.add(line); + } + } + + private Long glId(final String glCode) { + return this.glAccountRepository.findOneByGlCodeWithNotFoundDetection(glCode).getId(); + } + + /** Investments are a Head Office function, so the settlement entry is booked there. */ + private Long headOfficeId() { + return this.jdbcTemplate.queryForObject("SELECT id FROM m_office WHERE parent_id IS NULL ORDER BY id LIMIT 1", Long.class); + } + /** Govt Security / Bond: face value at a price per ₹100, plus interest owed to the seller since the last coupon. */ private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script) { final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE); @@ -172,9 +261,9 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur } final BigDecimal units = faceValue.divide(script.getFaceValuePerUnit(), 6, RoundingMode.HALF_UP); final BigDecimal cost = faceValue.multiply(rate).divide(HUNDRED, 2, RoundingMode.HALF_UP); - final int days = InvestmentInterestCalculator.days360(lastIntDate, p.getDealDate()); + // Broken-period interest runs to the settlement date, when the bond leaves the seller (RBI 83, Jitendra Sir). + final int days = InvestmentInterestCalculator.days360(lastIntDate, p.getSettlementDate()); final BigDecimal intRate = zeroIfNull(script.getIntRate()); - // Interest is counted up to the deal date (the event), not the settlement date — Jitendra Sir. final BigDecimal accrued = InvestmentInterestCalculator.interestForDays(faceValue, intRate, days); p.setFaceValue(faceValue); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index bffc1aa86..c48692b59 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -171,4 +171,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_investment_purchase_settlement.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_investment_purchase_settlement.xml new file mode 100644 index 000000000..671bee99b --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_investment_purchase_settlement.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code IN ('SETTLE_INVESTMENTPURCHASE', 'SETTLE_INVESTMENTPURCHASE_CHECKER') + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT rp.role_id, p.id + FROM m_role_permission rp + JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT' + CROSS JOIN m_permission p + WHERE p.code IN ('SETTLE_INVESTMENTPURCHASE', 'SETTLE_INVESTMENTPURCHASE_CHECKER') + AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id) + + +