diff --git a/deno.json b/deno.json index 43462ca..62abb3f 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@moonlight-protocol/provider-platform", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT", "exports": "./src/main.ts", "tasks": { diff --git a/src/core/service/auth/challenge/store/attach-entity-status.ts b/src/core/service/auth/challenge/store/attach-entity-status.ts index db3422f..31ff5a8 100644 --- a/src/core/service/auth/challenge/store/attach-entity-status.ts +++ b/src/core/service/auth/challenge/store/attach-entity-status.ts @@ -1,55 +1,71 @@ import { decode as decodeJwt } from "@zaubrik/djwt"; import { type MetadataHelper, ProcessEngine } from "@fifo/convee"; -import { AccountRepository } from "@/persistence/drizzle/repository/account.repository.ts"; -import { EntityRepository } from "@/persistence/drizzle/repository/entity.repository.ts"; +import { PpRepository } from "@/persistence/drizzle/repository/pp.repository.ts"; +import { PpEntityApprovalRepository } from "@/persistence/drizzle/repository/pp-entity-approval.repository.ts"; import { drizzleClient } from "@/persistence/drizzle/config.ts"; import { EntityStatus } from "@/persistence/drizzle/entity/entity.entity.ts"; import type { - ContextWithJWT, + ContextWithJWTAndPpPublicKey, ContextWithJWTAndStatus, } from "@/core/service/auth/challenge/types.ts"; import type { Logger } from "@/utils/logger/index.ts"; import { withSpan } from "@/core/tracing.ts"; -const accountRepository = new AccountRepository(drizzleClient); -const entityRepository = new EntityRepository(drizzleClient); +const ppRepository = new PpRepository(drizzleClient); +const ppEntityApprovalRepository = new PpEntityApprovalRepository( + drizzleClient, +); -// SEP-10 verify reports the submitter's entity status alongside the JWT. -// Unregistered submitters (no account row yet) report UNVERIFIED so the -// wallet can route them to the KYC submission step instead of letting them -// blind-submit a bundle that the bundle gate (BND_011) would reject. +// SEP-10 verify is PP-aware: the wallet posts `ppPublicKey` alongside the +// signed challenge, and this step reports the submitter's per-PP entity +// status + the operator-configured KYC submission URL for that PP. A wallet +// APPROVED on PP-A must not appear APPROVED on PP-B. export const P_AttachEntityStatus = (deps: { log: Logger }) => ProcessEngine.create( ( - input: ContextWithJWT, + input: ContextWithJWTAndPpPublicKey, _metadataHelper?: MetadataHelper, ): Promise => { return withSpan("P_AttachEntityStatus", async (span) => { const log = deps.log.scope("P_AttachEntityStatus"); - // We just minted this JWT in P_GenerateChallengeJWT; decode-without- - // verify is sufficient to read its own `sub` (the client account). + + const { ppPublicKey } = input; const [, payload] = decodeJwt(input.jwt); - const clientAccount = (payload as { sub?: string }).sub; - if (!clientAccount) { - log.event("no sub on JWT; reporting UNVERIFIED"); - return { ...input, entityStatus: EntityStatus.UNVERIFIED }; - } - span.addEvent("looking_up_entity", { - "client.account": clientAccount, + const accountPubkey = (payload as { sub?: string }).sub; + + const make = ( + entityStatus: EntityStatus, + kycSubmissionUrl: string | null, + ): ContextWithJWTAndStatus => ({ + ctx: input.ctx, + jwt: input.jwt, + entityStatus, + kycSubmissionUrl, }); - const account = await accountRepository.findById(clientAccount); - if (!account) { - log.event("no account record for submitter; reporting UNVERIFIED"); - span.addEvent("no_account"); - return { ...input, entityStatus: EntityStatus.UNVERIFIED }; + if (!accountPubkey) { + log.event("no sub on JWT; reporting UNVERIFIED + null URL"); + return make(EntityStatus.UNVERIFIED, null); } - const entity = await entityRepository.findById(account.entityId); - const entityStatus = entity?.status ?? EntityStatus.UNVERIFIED; + + span.addEvent("loading_pp", { "pp.public_key": ppPublicKey }); + const pp = await ppRepository.findByPublicKey(ppPublicKey); + if (!pp) { + log.event("pp not found; reporting UNVERIFIED + null URL"); + return make(EntityStatus.UNVERIFIED, null); + } + const kycSubmissionUrl = pp.kycSubmissionUrl ?? null; + + const approval = await ppEntityApprovalRepository.findByPpAndAccount( + ppPublicKey, + accountPubkey, + ); + const entityStatus = approval?.status ?? EntityStatus.UNVERIFIED; span.addEvent("entity_status_attached", { "entity.status": entityStatus, + "pp.has_kyc_url": String(kycSubmissionUrl !== null), }); - return { ...input, entityStatus }; + return make(entityStatus, kycSubmissionUrl); }); }, { diff --git a/src/core/service/auth/challenge/store/create-challenge-db.ts b/src/core/service/auth/challenge/store/create-challenge-db.ts index 062256d..f6bda53 100644 --- a/src/core/service/auth/challenge/store/create-challenge-db.ts +++ b/src/core/service/auth/challenge/store/create-challenge-db.ts @@ -5,7 +5,6 @@ import { EntityRepository } from "@/persistence/drizzle/repository/entity.reposi import { AccountRepository } from "@/persistence/drizzle/repository/account.repository.ts"; import { ChallengeStatus, - EntityStatus, type NewAccount, type NewChallenge, type NewEntity, @@ -44,7 +43,6 @@ export const P_CreateChallengeDB = (deps: { log: Logger }) => log.event("creating new entity and account"); entity = await entityRepository.create({ id: crypto.randomUUID(), - status: EntityStatus.UNVERIFIED, } as NewEntity); account = await accountRepository.create({ diff --git a/src/core/service/auth/challenge/store/update-challenge-db.ts b/src/core/service/auth/challenge/store/update-challenge-db.ts index f74bb9d..0dc5e0d 100644 --- a/src/core/service/auth/challenge/store/update-challenge-db.ts +++ b/src/core/service/auth/challenge/store/update-challenge-db.ts @@ -5,7 +5,7 @@ import { drizzleClient } from "@/persistence/drizzle/config.ts"; import { ChallengeRepository } from "@/persistence/drizzle/repository/challenge.repository.ts"; import { ChallengeStatus } from "@/persistence/drizzle/entity/challenge.entity.ts"; import type { - ContextWithJWT, + ContextWithJWTAndPpPublicKey, PostChallengeWithJWT, } from "@/core/service/auth/challenge/types.ts"; import * as E from "@/core/service/auth/challenge/store/error.ts"; @@ -18,12 +18,12 @@ const challengeRepository = new ChallengeRepository(drizzleClient); export const P_UpdateChallengeDB = (deps: { log: Logger }) => ProcessEngine.create( - (input: PostChallengeWithJWT): Promise => { + (input: PostChallengeWithJWT): Promise => { return withSpan("P_UpdateChallengeDB", async (span) => { const log = deps.log.scope("P_UpdateChallengeDB"); log.info("P_UpdateChallengeDB"); - const { signedChallenge } = input.body; + const { signedChallenge, ppPublicKey } = input.body; const tx = new Transaction( signedChallenge, NETWORK_CONFIG.networkPassphrase, @@ -51,7 +51,8 @@ export const P_UpdateChallengeDB = (deps: { log: Logger }) => return { ctx: input.ctx, - jwt: input.jwt!, + jwt: input.jwt, + ppPublicKey, }; }); }, diff --git a/src/core/service/auth/challenge/types.ts b/src/core/service/auth/challenge/types.ts index 2fc1b93..236c32e 100644 --- a/src/core/service/auth/challenge/types.ts +++ b/src/core/service/auth/challenge/types.ts @@ -10,15 +10,19 @@ import type { EntityStatus } from "@/persistence/drizzle/entity/entity.entity.ts export type PostChallengeInput = PostEndpointInput; export type PostChallengeWithJWT = PostChallengeInput & { jwt: string }; -export type PostChallengeWithEntityStatus = PostChallengeWithJWT & { - entityStatus: EntityStatus; -}; export type ContextWithJWT = { ctx: Context; jwt: string; }; +// P_UpdateChallengeDB narrows from PostChallengeWithJWT to drop the full +// request body, forwarding only the single field (`ppPublicKey`) that +// P_AttachEntityStatus needs. Principle of least information. +export type ContextWithJWTAndPpPublicKey = ContextWithJWT & { + ppPublicKey: string; +}; export type ContextWithJWTAndStatus = ContextWithJWT & { entityStatus: EntityStatus; + kycSubmissionUrl: string | null; }; export type ChallengeData = { diff --git a/src/core/service/bundle/add-bundle.process.ts b/src/core/service/bundle/add-bundle.process.ts index a8935c5..c9e4336 100644 --- a/src/core/service/bundle/add-bundle.process.ts +++ b/src/core/service/bundle/add-bundle.process.ts @@ -38,6 +38,7 @@ import { SessionRepository, UtxoRepository, } from "@/persistence/drizzle/repository/index.ts"; +import { PpEntityApprovalRepository } from "@/persistence/drizzle/repository/pp-entity-approval.repository.ts"; import { EntityStatus } from "@/persistence/drizzle/entity/index.ts"; import { withSpan } from "@/core/tracing.ts"; import type { Logger } from "@/utils/logger/index.ts"; @@ -45,6 +46,7 @@ import type { Logger } from "@/utils/logger/index.ts"; const sessionRepository = new SessionRepository(drizzleClient); const accountRepository = new AccountRepository(drizzleClient); const entityRepository = new EntityRepository(drizzleClient); +const ppApprovalRepository = new PpEntityApprovalRepository(drizzleClient); const utxoRepository = new UtxoRepository(drizzleClient); const operationsBundleRepository = new OperationsBundleRepository( drizzleClient, @@ -342,21 +344,30 @@ export const P_AddOperationsBundle = (deps: { log: Logger }) => span.addEvent("validating_entity_approval", { "account.id": userSession.accountId, + "pp.public_key": ppPublicKey, }); - log.event("validating entity approval"); + log.event("validating per-PP entity approval"); const submitterAccount = await accountRepository.findById( userSession.accountId, ); - const submitterEntity = submitterAccount - ? await entityRepository.findById(submitterAccount.entityId) - : null; - if ( - !submitterEntity || - submitterEntity.status !== EntityStatus.APPROVED - ) { - log.event("submitter entity not approved"); + if (!submitterAccount) { + log.event("submitter has no account; reject"); throw new E.SUBMITTER_NOT_APPROVED(userSession.accountId); } + const approval = await ppApprovalRepository.findByPpAndAccount( + ppPublicKey, + submitterAccount.id, + ); + if (!approval || approval.status !== EntityStatus.APPROVED) { + log.event("submitter entity not approved for this PP"); + throw new E.SUBMITTER_NOT_APPROVED(userSession.accountId); + } + // Identity (name / jurisdictions) still lives on the global entity + // record; the per-PP approval above is the gate, but the bundle entry + // wants the submitter's identity for downstream audit/labels. + const submitterEntity = await entityRepository.findById( + submitterAccount.entityId, + ); span.addEvent("generating_bundle_id"); log.event("generating bundle ID"); diff --git a/src/http/v1/dashboard/pp.ts b/src/http/v1/dashboard/pp.ts index c3737f3..813a450 100644 --- a/src/http/v1/dashboard/pp.ts +++ b/src/http/v1/dashboard/pp.ts @@ -26,7 +26,7 @@ export function handleRegisterPp( log.info("registerPp"); try { const body = await ctx.request.body.json(); - const { secretKey, derivationIndex, label } = body; + const { secretKey, derivationIndex, label, kycSubmissionUrl } = body; if (!secretKey || typeof secretKey !== "string") { ctx.response.status = Status.BadRequest; @@ -78,6 +78,10 @@ export function handleRegisterPp( ownerPublicKey, isActive: true, label: label?.trim() ?? null, + kycSubmissionUrl: typeof kycSubmissionUrl === "string" && + kycSubmissionUrl.trim().length > 0 + ? kycSubmissionUrl.trim() + : null, createdAt: new Date(), updatedAt: new Date(), }); diff --git a/src/http/v1/entities/post.ts b/src/http/v1/entities/post.ts index ad888ab..a01d02a 100644 --- a/src/http/v1/entities/post.ts +++ b/src/http/v1/entities/post.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { drizzleClient } from "@/persistence/drizzle/config.ts"; import { AccountRepository } from "@/persistence/drizzle/repository/account.repository.ts"; import { EntityRepository } from "@/persistence/drizzle/repository/entity.repository.ts"; +import { PpEntityApprovalRepository } from "@/persistence/drizzle/repository/pp-entity-approval.repository.ts"; import { EntityStatus, type NewAccount, @@ -13,6 +14,7 @@ import type { Logger } from "@/utils/logger/index.ts"; const entityRepo = new EntityRepository(drizzleClient); const accountRepo = new AccountRepository(drizzleClient); +const ppApprovalRepo = new PpEntityApprovalRepository(drizzleClient); const NAME_MAX_LEN = 250; @@ -52,12 +54,9 @@ function sanitiseName(raw: string): string { * * Public KYC/KYB-style submission. Caller proves wallet ownership by signing * a previously-issued nonce (see POST /entities/challenge). On success the - * entity is created or its existing record promoted to APPROVED, and a - * USER-type account is created for the pubkey if one doesn't exist yet. - * - * The :ppPublicKey URL param is intentionally not loaded here — the PP exists - * (the route mount verified that). What matters is the submitter's wallet - * proof and the sanitised name/jurisdictions. + * entity is created/updated AND a per-PP approval row is upserted to APPROVED + * for the pair (ppPublicKey, pubkey). Identity (name, jurisdictions) stays on + * the global entity record; the per-PP gate lives on `pp_entity_approvals`. */ export function handlePostEntity( deps: { log: Logger }, @@ -67,6 +66,14 @@ export function handlePostEntity( return async (ctx) => { log.info("postEntity"); try { + const params = + (ctx as unknown as { params?: { ppPublicKey?: string } }).params; + const ppPublicKey = params?.ppPublicKey; + if (!ppPublicKey || typeof ppPublicKey !== "string") { + ctx.response.status = Status.BadRequest; + ctx.response.body = { message: "ppPublicKey path param is required" }; + return; + } const raw = await ctx.request.body.json(); const parsed = bodySchema.safeParse(raw); if (!parsed.success) { @@ -105,65 +112,79 @@ export function handlePostEntity( } log.debug("pubkey", pubkey); + log.debug("ppPublicKey", ppPublicKey); log.debug("nameLength", name.length); - log.event("checking for existing account"); + // Identity record (global): create-or-update the entity + account. + let entityId: string; const existingAccount = await accountRepo.findById(pubkey); - if (existingAccount) { - const existingEntity = await entityRepo.findById( - existingAccount.entityId, - ); - if (existingEntity?.status === EntityStatus.APPROVED) { - log.event("entity already approved for pubkey"); + log.event("updating existing entity identity"); + const updated = await entityRepo.update(existingAccount.entityId, { + name, + jurisdictions, + }); + entityId = updated?.id ?? existingAccount.entityId; + } else { + log.event("creating new entity + account"); + const newEntity = await entityRepo.create({ + id: crypto.randomUUID(), + status: EntityStatus.UNVERIFIED, + name, + jurisdictions, + } as NewEntity); + await accountRepo.create({ + id: pubkey, + type: "USER", + entityId: newEntity.id, + } as NewAccount); + entityId = newEntity.id; + } + + // Per-PP approval: upsert this (pp, account) pair to APPROVED. + const existingApproval = await ppApprovalRepo.findByPpAndAccount( + ppPublicKey, + pubkey, + ); + if (existingApproval) { + if (existingApproval.status === EntityStatus.APPROVED) { + log.event("pp approval already approved for pubkey"); ctx.response.status = Status.Conflict; ctx.response.body = { - message: "Entity already approved for this pubkey", - data: { pubkey, entityId: existingEntity.id }, + message: "Entity already approved for this provider", + data: { + pubkey, + ppPublicKey, + entityId, + status: EntityStatus.APPROVED, + }, }; return; } - log.event("promoting existing entity to APPROVED"); - const updated = await entityRepo.update(existingAccount.entityId, { - name, - jurisdictions, + log.event("promoting existing pp approval to APPROVED"); + await ppApprovalRepo.update(existingApproval.id, { + status: EntityStatus.APPROVED, + }); + } else { + log.event("creating new pp approval (APPROVED)"); + await ppApprovalRepo.create({ + id: crypto.randomUUID(), + ppPublicKey, + accountPubkey: pubkey, status: EntityStatus.APPROVED, }); - log.debug("entityId", updated?.id ?? existingAccount.entityId); - ctx.response.status = Status.OK; - ctx.response.body = { - message: "Entity updated", - data: { - pubkey, - entityId: updated?.id ?? existingAccount.entityId, - status: EntityStatus.APPROVED, - }, - }; - return; } - log.event("creating new entity + account"); - const newEntity = await entityRepo.create({ - id: crypto.randomUUID(), - status: EntityStatus.APPROVED, - name, - jurisdictions, - } as NewEntity); - - await accountRepo.create({ - id: pubkey, - type: "USER", - entityId: newEntity.id, - } as NewAccount); - - log.debug("entityId", newEntity.id); - log.event("entity registered and approved"); - ctx.response.status = Status.Created; + log.event("entity approved for pp"); + ctx.response.status = existingApproval ? Status.OK : Status.Created; ctx.response.body = { - message: "Entity created", + message: existingApproval + ? "Entity approval updated" + : "Entity approved", data: { pubkey, - entityId: newEntity.id, + ppPublicKey, + entityId, status: EntityStatus.APPROVED, }, }; diff --git a/src/http/v1/stellar/auth/post.ts b/src/http/v1/stellar/auth/post.ts index 378003b..29cf8fb 100644 --- a/src/http/v1/stellar/auth/post.ts +++ b/src/http/v1/stellar/auth/post.ts @@ -12,13 +12,19 @@ import type { ContextWithJWTAndStatus } from "@/core/service/auth/challenge/type import { EntityStatus } from "@/persistence/drizzle/entity/entity.entity.ts"; import type { Logger } from "@/utils/logger/index.ts"; +// SEP-10 verify is now PP-aware: the wallet posts `ppPublicKey` alongside +// the signed challenge so the response can carry per-PP `entityStatus` and +// per-PP `kycSubmissionUrl`. A wallet APPROVED on PP-A must not appear +// APPROVED on PP-B. export const requestSchema = z.object({ signedChallenge: z.string(), + ppPublicKey: z.string().min(1), }); export const responseSchema = z.object({ jwt: z.string(), entityStatus: z.nativeEnum(EntityStatus), + kycSubmissionUrl: z.string().nullable(), }); export function handlePostAuth( @@ -38,6 +44,7 @@ export function handlePostAuth( data: { jwt: input.jwt, entityStatus: input.entityStatus, + kycSubmissionUrl: input.kycSubmissionUrl, }, }; }; diff --git a/src/persistence/drizzle/entity/entity.entity.ts b/src/persistence/drizzle/entity/entity.entity.ts index 0b35aed..99f1b3f 100644 --- a/src/persistence/drizzle/entity/entity.entity.ts +++ b/src/persistence/drizzle/entity/entity.entity.ts @@ -4,6 +4,9 @@ import { createBaseColumns } from "@/persistence/drizzle/entity/base.entity.ts"; import { account } from "@/persistence/drizzle/entity/account.entity.ts"; import { challenge } from "@/persistence/drizzle/entity/challenge.entity.ts"; +// Identity-level status enum reused by `pp_entity_approvals.status` to gate +// bundle submission per PP. The global `entities` table owns identity +// (name, jurisdictions) only — there is no global status column anymore. export enum EntityStatus { UNVERIFIED = "UNVERIFIED", APPROVED = "APPROVED", @@ -20,7 +23,6 @@ export const entityStatusEnum = pgEnum("entity_status", [ export const entity = pgTable("entities", { id: text("id").primaryKey(), - status: entityStatusEnum("status").notNull(), name: text("name"), jurisdictions: text("jurisdictions").array(), ...createBaseColumns(), diff --git a/src/persistence/drizzle/entity/index.ts b/src/persistence/drizzle/entity/index.ts index a96ee81..c4ef681 100644 --- a/src/persistence/drizzle/entity/index.ts +++ b/src/persistence/drizzle/entity/index.ts @@ -17,3 +17,4 @@ export * from "@/persistence/drizzle/entity/council-membership.entity.ts"; export * from "@/persistence/drizzle/entity/pp.entity.ts"; export * from "@/persistence/drizzle/entity/wallet-user.entity.ts"; export * from "@/persistence/drizzle/entity/waitlist-request.entity.ts"; +export * from "@/persistence/drizzle/entity/pp-entity-approval.entity.ts"; diff --git a/src/persistence/drizzle/entity/pp-entity-approval.entity.ts b/src/persistence/drizzle/entity/pp-entity-approval.entity.ts new file mode 100644 index 0000000..4c3f118 --- /dev/null +++ b/src/persistence/drizzle/entity/pp-entity-approval.entity.ts @@ -0,0 +1,21 @@ +import { pgTable, text, uniqueIndex } from "drizzle-orm/pg-core"; +import { createBaseColumns } from "@/persistence/drizzle/entity/base.entity.ts"; +import { entityStatusEnum } from "@/persistence/drizzle/entity/entity.entity.ts"; + +// Per-PP entity approval. Replaces the global entity.status as the gate for +// bundle submission: a wallet APPROVED on PP-A must not appear APPROVED on +// PP-B. The entity table still owns identity (name/jurisdictions) — only the +// per-PP status moves here. +export const ppEntityApproval = pgTable("pp_entity_approvals", { + id: text("id").primaryKey(), + ppPublicKey: text("pp_public_key").notNull(), + accountPubkey: text("account_pubkey").notNull(), + status: entityStatusEnum("status").notNull(), + ...createBaseColumns(), +}, (t) => ({ + ppAccountUnique: uniqueIndex("pp_entity_approvals_pp_account_unique") + .on(t.ppPublicKey, t.accountPubkey), +})); + +export type PpEntityApproval = typeof ppEntityApproval.$inferSelect; +export type NewPpEntityApproval = typeof ppEntityApproval.$inferInsert; diff --git a/src/persistence/drizzle/entity/pp.entity.ts b/src/persistence/drizzle/entity/pp.entity.ts index 7d029f6..54d4918 100644 --- a/src/persistence/drizzle/entity/pp.entity.ts +++ b/src/persistence/drizzle/entity/pp.entity.ts @@ -9,6 +9,11 @@ export const paymentProvider = pgTable("payment_providers", { ownerPublicKey: text("owner_public_key"), isActive: boolean("is_active").notNull().default(false), label: text("label"), + // Optional. When set, the wallet renders a "Submit KYC" link pointing at + // this URL whenever the submitter's per-PP entity status is not APPROVED. + // Operator-owned (different PP operators may use different schemes), so + // the wallet uses the stored string verbatim. + kycSubmissionUrl: text("kyc_submission_url"), ...createBaseColumns(), }); diff --git a/src/persistence/drizzle/migration/0017_pp_aware_entity_status_and_kyc_url.sql b/src/persistence/drizzle/migration/0017_pp_aware_entity_status_and_kyc_url.sql new file mode 100644 index 0000000..ec583d7 --- /dev/null +++ b/src/persistence/drizzle/migration/0017_pp_aware_entity_status_and_kyc_url.sql @@ -0,0 +1,24 @@ +-- Per-PP entity approvals + per-PP KYC submission URL. +-- The global entity.status column is dropped in the same migration — no +-- bandaid column left for a future reader to misuse. Bundle gate now +-- queries pp_entity_approvals.status per (pp_public_key, account_pubkey). + +ALTER TABLE "payment_providers" ADD COLUMN "kyc_submission_url" text; + +ALTER TABLE "entities" DROP COLUMN "status"; + +CREATE TABLE "pp_entity_approvals" ( + "id" text PRIMARY KEY NOT NULL, + "pp_public_key" text NOT NULL, + "account_pubkey" text NOT NULL, + "status" "entity_status" NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_by" text, + "updated_by" text, + "deleted_at" timestamp with time zone, + CONSTRAINT "pp_entity_approvals_pp_account_unique" UNIQUE("pp_public_key","account_pubkey") +); + +CREATE INDEX IF NOT EXISTS "idx_pp_entity_approvals_lookup" + ON "pp_entity_approvals" ("pp_public_key", "account_pubkey"); diff --git a/src/persistence/drizzle/migration/meta/_journal.json b/src/persistence/drizzle/migration/meta/_journal.json index cfce683..1e64efe 100644 --- a/src/persistence/drizzle/migration/meta/_journal.json +++ b/src/persistence/drizzle/migration/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1780200000000, "tag": "0016_bundle_pp_public_key", "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1780300000000, + "tag": "0017_pp_aware_entity_status_and_kyc_url", + "breakpoints": true } ] } diff --git a/src/persistence/drizzle/repository/entity.repository.ts b/src/persistence/drizzle/repository/entity.repository.ts index 0e7c1c7..aa687a0 100644 --- a/src/persistence/drizzle/repository/entity.repository.ts +++ b/src/persistence/drizzle/repository/entity.repository.ts @@ -1,9 +1,7 @@ -import { and, eq, isNull } from "drizzle-orm"; import type { DrizzleClient } from "@/persistence/drizzle/config.ts"; import { type Entity, entity, - type EntityStatus, type NewEntity, } from "@/persistence/drizzle/entity/entity.entity.ts"; import { BaseRepository } from "@/persistence/drizzle/repository/base.repository.ts"; @@ -13,14 +11,4 @@ export class EntityRepository constructor(db: DrizzleClient) { super(db, entity); } - - /** - * Finds entities by status - */ - async findByStatus(status: EntityStatus) { - return await this.db - .select() - .from(entity) - .where(and(eq(entity.status, status), isNull(entity.deletedAt))); - } } diff --git a/src/persistence/drizzle/repository/pp-entity-approval.repository.ts b/src/persistence/drizzle/repository/pp-entity-approval.repository.ts new file mode 100644 index 0000000..467f5ee --- /dev/null +++ b/src/persistence/drizzle/repository/pp-entity-approval.repository.ts @@ -0,0 +1,36 @@ +import { and, eq, isNull } from "drizzle-orm"; +import type { DrizzleClient } from "@/persistence/drizzle/config.ts"; +import { + type NewPpEntityApproval, + type PpEntityApproval, + ppEntityApproval, +} from "@/persistence/drizzle/entity/pp-entity-approval.entity.ts"; +import { BaseRepository } from "@/persistence/drizzle/repository/base.repository.ts"; + +export class PpEntityApprovalRepository extends BaseRepository< + typeof ppEntityApproval, + PpEntityApproval, + NewPpEntityApproval +> { + constructor(db: DrizzleClient) { + super(db, ppEntityApproval); + } + + async findByPpAndAccount( + ppPublicKey: string, + accountPubkey: string, + ): Promise { + const [row] = await this.db + .select() + .from(ppEntityApproval) + .where( + and( + eq(ppEntityApproval.ppPublicKey, ppPublicKey), + eq(ppEntityApproval.accountPubkey, accountPubkey), + isNull(ppEntityApproval.deletedAt), + ), + ) + .limit(1); + return row; + } +}