feat: redesign Prisma schema for full domain model#18
Conversation
- Replace prisma.merchantSession with prisma.refreshToken following MerchantSession → RefreshToken rename - Use merchant.registered instead of firstName !== null to determine isRegistered in authenticateWallet response
|
Hello @G-ELM |
codebestia
left a comment
There was a problem hiding this comment.
@G-ELM
I reviewed your code and there are some missing implementations.
You need to properly implement the invoice schema according to the structure of the smart contract.
You can get it from here. https://github.com/ShadeProtocol/shade-stellar-contract/blob/main/contracts/shade/src/types.rs#L114
Take into account the pricing mode, the fiat pricing e.t.c
https://github.com/ShadeProtocol/shade-stellar-contract/blob/main/contracts/shade/src/types.rs
Also create schema for the following types in the contract.
The subscription, the analytics and the transaction.
Ensure to add tests to verify the implementations.
|
GM @codebestia |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe PR replaces the draft Prisma schema and migration with a full init schema, updates auth to use RefreshToken instead of MerchantSession, and adds unit and integration tests for the new invoice, subscription, analytics, and bridge payment models. ChangesSchema redesign and auth migration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
tests/unit/analytics.schema.test.tsOops! Something went wrong! :( ESLint: 9.24.0 ReferenceError: require is not defined in ES module scope, you can use import instead tests/unit/invoice.schema.test.tsOops! Something went wrong! :( ESLint: 9.24.0 ReferenceError: require is not defined in ES module scope, you can use import instead tests/unit/subscription.schema.test.tsOops! Something went wrong! :( ESLint: 9.24.0 ReferenceError: require is not defined in ES module scope, you can use import instead Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@prisma/schema.prisma`:
- Around line 88-97: The InvoiceStatus enum is missing the EXPIRED value
required by the contract, so update the enum definition in InvoiceStatus to
include EXPIRED alongside the existing statuses. Make sure the Prisma schema
stays aligned with the smart contract and any generated client or migration
logic that relies on InvoiceStatus can represent expired invoices correctly.
- Around line 165-171: The Subscription model currently validates planId and
merchantId separately, so a subscription can point to a plan and merchant from
different tenants. Update the Prisma schema in the Subscription and related
SubscriptionPlan/Merchant relations so the tenant is enforced at the database
level, either by deriving merchant from plan or by using a composite
relation/foreign key that checks planId and merchantId together. Use the
Subscription, SubscriptionPlan, and Merchant relation definitions to locate and
adjust the schema.
- Around line 207-212: The BridgePayment model currently allows invoiceId and
merchantId to point to different merchants, so update the Prisma schema to
enforce tenant consistency between BridgePayment, Invoice, and Merchant. Adjust
the BridgePayment relation in prisma/schema.prisma so the invoice association
cannot be created unless it matches the same merchant as the bridge payment,
using the model’s existing relation fields (BridgePayment, Invoice, Merchant)
and any needed composite relation or uniqueness constraint to prevent
cross-merchant mismatches.
In `@tests/unit/invoice.schema.test.ts`:
- Around line 52-282: The Invoice schema tests are only validating mocked Prisma
model calls, so they do not verify the actual schema or migrations. Replace the
prismaMock-based assertions in the Invoice schema suite with integration tests
using a real PrismaClient against a test database, and exercise the actual
invoice create/update/findUnique behavior for defaults, enum transitions, unique
constraints, and merchant relation loading so the schema itself is validated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 22382a9f-1a10-4c83-8ed1-77d7368de1c4
📒 Files selected for processing (11)
prisma/migrations/20260621183614_demo/migration.sqlprisma/migrations/20260623000000_init/migration.sqlprisma/schema.prismasrc/middlewares/auth.middleware.tssrc/services/auth.services.tstests/integration/auth.routes.test.tstests/integration/merchant.register.test.tstests/unit/analytics.schema.test.tstests/unit/auth.services.test.tstests/unit/invoice.schema.test.tstests/unit/subscription.schema.test.ts
💤 Files with no reviewable changes (1)
- prisma/migrations/20260621183614_demo/migration.sql
| // Matches the smart contract InvoiceStatus enum exactly. | ||
| enum InvoiceStatus { | ||
| PENDING | ||
| PAID | ||
| CANCELLED | ||
| REFUNDED | ||
| PARTIALLY_REFUNDED | ||
| PARTIALLY_PAID | ||
| DRAFT | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add the missing EXPIRED invoice status.
The contract in this PR requires InvoiceStatus.EXPIRED, but the enum here omits it, so the generated client and baseline migration cannot represent expired invoices at all.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prisma/schema.prisma` around lines 88 - 97, The InvoiceStatus enum is missing
the EXPIRED value required by the contract, so update the enum definition in
InvoiceStatus to include EXPIRED alongside the existing statuses. Make sure the
Prisma schema stays aligned with the smart contract and any generated client or
migration logic that relies on InvoiceStatus can represent expired invoices
correctly.
| describe('Invoice schema', () => { | ||
| beforeEach(() => { | ||
| mockReset(prismaMock); | ||
| }); | ||
|
|
||
| describe('create', () => { | ||
| test('creates a DRAFT invoice with FIXED_CRYPTO pricing', async () => { | ||
| prismaMock.invoice.create.mockResolvedValue(baseInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.create({ | ||
| data: { | ||
| invoiceId: 1001, | ||
| paymentSlug: 'pay-abc123', | ||
| description: 'Payment for services', | ||
| amount: BigInt(1_000_000), | ||
| token: 'CABC...TOKEN', | ||
| merchantId: 'merchant-uuid', | ||
| status: InvoiceStatus.DRAFT, | ||
| pricingMode: InvoicePricingMode.FIXED_CRYPTO, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.status).toBe(InvoiceStatus.DRAFT); | ||
| expect(result.pricingMode).toBe(InvoicePricingMode.FIXED_CRYPTO); | ||
| expect(result.amountPaid).toBe(BigInt(0)); | ||
| expect(result.amountRefunded).toBe(BigInt(0)); | ||
| expect(prismaMock.invoice.create).toHaveBeenCalledWith({ | ||
| data: expect.objectContaining({ | ||
| invoiceId: 1001, | ||
| paymentSlug: 'pay-abc123', | ||
| description: 'Payment for services', | ||
| }), | ||
| }); | ||
| }); | ||
|
|
||
| test('creates a FIXED_FIAT invoice with fiat pricing fields populated', async () => { | ||
| const fiatInvoice = { | ||
| ...baseInvoice, | ||
| pricingMode: InvoicePricingMode.FIXED_FIAT, | ||
| fiatCurrency: 'USD', | ||
| fiatAmount: BigInt(500_00), | ||
| fiatDecimals: 2, | ||
| }; | ||
| prismaMock.invoice.create.mockResolvedValue(fiatInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.create({ | ||
| data: { | ||
| invoiceId: 1002, | ||
| paymentSlug: 'pay-def456', | ||
| description: 'USD-pegged invoice', | ||
| amount: BigInt(1_000_000), | ||
| token: 'CABC...TOKEN', | ||
| merchantId: 'merchant-uuid', | ||
| status: InvoiceStatus.PENDING, | ||
| pricingMode: InvoicePricingMode.FIXED_FIAT, | ||
| fiatCurrency: 'USD', | ||
| fiatAmount: BigInt(500_00), | ||
| fiatDecimals: 2, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.pricingMode).toBe(InvoicePricingMode.FIXED_FIAT); | ||
| expect(result.fiatCurrency).toBe('USD'); | ||
| expect(result.fiatAmount).toBe(BigInt(500_00)); | ||
| expect(result.fiatDecimals).toBe(2); | ||
| }); | ||
|
|
||
| test('creates an invoice with an expiry date', async () => { | ||
| const expiresAt = new Date(mockDate.getTime() + 24 * 60 * 60 * 1000); | ||
| const expiringInvoice = { ...baseInvoice, expiresAt }; | ||
| prismaMock.invoice.create.mockResolvedValue(expiringInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.create({ | ||
| data: { | ||
| ...baseInvoice, | ||
| expiresAt, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.expiresAt).toEqual(expiresAt); | ||
| }); | ||
| }); | ||
|
|
||
| describe('InvoiceStatus transitions', () => { | ||
| test.each([ | ||
| [InvoiceStatus.PENDING], | ||
| [InvoiceStatus.PAID], | ||
| [InvoiceStatus.CANCELLED], | ||
| [InvoiceStatus.REFUNDED], | ||
| [InvoiceStatus.PARTIALLY_REFUNDED], | ||
| [InvoiceStatus.PARTIALLY_PAID], | ||
| [InvoiceStatus.DRAFT], | ||
| ])('accepts status %s', async (status) => { | ||
| prismaMock.invoice.update.mockResolvedValue({ ...baseInvoice, status }); | ||
|
|
||
| const result = await prismaMock.invoice.update({ | ||
| where: { id: 'invoice-uuid' }, | ||
| data: { status }, | ||
| }); | ||
|
|
||
| expect(result.status).toBe(status); | ||
| }); | ||
|
|
||
| test('records amountPaid and datePaid when status transitions to PAID', async () => { | ||
| const paidInvoice = { | ||
| ...baseInvoice, | ||
| status: InvoiceStatus.PAID, | ||
| amountPaid: BigInt(1_000_000), | ||
| datePaid: mockDate, | ||
| }; | ||
| prismaMock.invoice.update.mockResolvedValue(paidInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.update({ | ||
| where: { id: 'invoice-uuid' }, | ||
| data: { | ||
| status: InvoiceStatus.PAID, | ||
| amountPaid: BigInt(1_000_000), | ||
| datePaid: mockDate, | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.status).toBe(InvoiceStatus.PAID); | ||
| expect(result.amountPaid).toBe(BigInt(1_000_000)); | ||
| expect(result.datePaid).toEqual(mockDate); | ||
| }); | ||
|
|
||
| test('records amountRefunded when status transitions to REFUNDED', async () => { | ||
| const refundedInvoice = { | ||
| ...baseInvoice, | ||
| status: InvoiceStatus.REFUNDED, | ||
| amountPaid: BigInt(1_000_000), | ||
| amountRefunded: BigInt(1_000_000), | ||
| datePaid: mockDate, | ||
| }; | ||
| prismaMock.invoice.update.mockResolvedValue(refundedInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.update({ | ||
| where: { id: 'invoice-uuid' }, | ||
| data: { | ||
| status: InvoiceStatus.REFUNDED, | ||
| amountRefunded: BigInt(1_000_000), | ||
| }, | ||
| }); | ||
|
|
||
| expect(result.status).toBe(InvoiceStatus.REFUNDED); | ||
| expect(result.amountRefunded).toBe(BigInt(1_000_000)); | ||
| }); | ||
|
|
||
| test('records partial amounts for PARTIALLY_PAID status', async () => { | ||
| const partialInvoice = { | ||
| ...baseInvoice, | ||
| status: InvoiceStatus.PARTIALLY_PAID, | ||
| amountPaid: BigInt(500_000), | ||
| }; | ||
| prismaMock.invoice.update.mockResolvedValue(partialInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.update({ | ||
| where: { id: 'invoice-uuid' }, | ||
| data: { status: InvoiceStatus.PARTIALLY_PAID, amountPaid: BigInt(500_000) }, | ||
| }); | ||
|
|
||
| expect(result.status).toBe(InvoiceStatus.PARTIALLY_PAID); | ||
| expect(result.amountPaid).toBe(BigInt(500_000)); | ||
| }); | ||
| }); | ||
|
|
||
| describe('unique constraints', () => { | ||
| test('findUnique by invoiceId returns the invoice', async () => { | ||
| prismaMock.invoice.findUnique.mockResolvedValue(baseInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.findUnique({ where: { invoiceId: 1001 } }); | ||
|
|
||
| expect(result).toEqual(baseInvoice); | ||
| expect(prismaMock.invoice.findUnique).toHaveBeenCalledWith({ | ||
| where: { invoiceId: 1001 }, | ||
| }); | ||
| }); | ||
|
|
||
| test('findUnique by paymentSlug returns the invoice', async () => { | ||
| prismaMock.invoice.findUnique.mockResolvedValue(baseInvoice); | ||
|
|
||
| const result = await prismaMock.invoice.findUnique({ where: { paymentSlug: 'pay-abc123' } }); | ||
|
|
||
| expect(result).toEqual(baseInvoice); | ||
| }); | ||
|
|
||
| test('rejects duplicate paymentSlug with P2002', async () => { | ||
| const uniqueError = Object.assign(new Error('Unique constraint failed'), { | ||
| code: 'P2002', | ||
| meta: { target: ['paymentSlug'] }, | ||
| }); | ||
| prismaMock.invoice.create.mockRejectedValue(uniqueError); | ||
|
|
||
| await expect( | ||
| prismaMock.invoice.create({ | ||
| data: { ...baseInvoice, id: 'other-uuid', invoiceId: 9999 }, | ||
| }), | ||
| ).rejects.toMatchObject({ code: 'P2002', meta: { target: ['paymentSlug'] } }); | ||
| }); | ||
|
|
||
| test('rejects duplicate invoiceId with P2002', async () => { | ||
| const uniqueError = Object.assign(new Error('Unique constraint failed'), { | ||
| code: 'P2002', | ||
| meta: { target: ['invoiceId'] }, | ||
| }); | ||
| prismaMock.invoice.create.mockRejectedValue(uniqueError); | ||
|
|
||
| await expect( | ||
| prismaMock.invoice.create({ | ||
| data: { ...baseInvoice, id: 'other-uuid', paymentSlug: 'pay-new' }, | ||
| }), | ||
| ).rejects.toMatchObject({ code: 'P2002', meta: { target: ['invoiceId'] } }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('merchant relation', () => { | ||
| test('findUnique with merchant include returns nested merchant', async () => { | ||
| prismaMock.invoice.findUnique.mockResolvedValue({ | ||
| ...baseInvoice, | ||
| merchant: baseMerchant, | ||
| }); | ||
|
|
||
| const result = await prismaMock.invoice.findUnique({ | ||
| where: { id: 'invoice-uuid' }, | ||
| include: { merchant: true }, | ||
| }); | ||
|
|
||
| expect(result?.merchant).toMatchObject({ id: 'merchant-uuid' }); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
These tests do not exercise the Prisma schema.
Because the suite only asserts values you preloaded into prismaMock, it will still pass when the real schema or migration is wrong. For defaults, enums, unique constraints, and relations, this needs a real PrismaClient against a test database instead of mocked model methods.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/unit/invoice.schema.test.ts` around lines 52 - 282, The Invoice schema
tests are only validating mocked Prisma model calls, so they do not verify the
actual schema or migrations. Replace the prismaMock-based assertions in the
Invoice schema suite with integration tests using a real PrismaClient against a
test database, and exercise the actual invoice create/update/findUnique behavior
for defaults, enum transitions, unique constraints, and merchant relation
loading so the schema itself is validated.
|
@G-ELM |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/services/invoice.services.ts`:
- Around line 58-82: The retry loop in create invoice payment slug generation
currently rethrows the raw Prisma unique constraint error on the last collision,
making the AppError(500, 'Failed to generate a unique payment slug') fallback
unreachable. Update the catch logic in src/services/invoice.services.ts around
the prisma.invoice.create and isUniqueSlugError path so that slug-collision
errors on the final attempt exit the loop and allow the fallback AppError to be
thrown, while still rethrowing non-slug-related errors immediately.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3ba5efa0-bd63-43c3-89c6-0e86e4972dc4
📒 Files selected for processing (5)
prisma/migrations/20260623000000_init/migration.sqlprisma/schema.prismasrc/services/invoice.services.tssrc/utils/invoice.validation.tstests/integration/invoice.routes.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- prisma/migrations/20260623000000_init/migration.sql
- prisma/schema.prisma
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/services/invoice.services.ts`:
- Around line 58-82: The retry loop in create invoice payment slug generation
currently rethrows the raw Prisma unique constraint error on the last collision,
making the AppError(500, 'Failed to generate a unique payment slug') fallback
unreachable. Update the catch logic in src/services/invoice.services.ts around
the prisma.invoice.create and isUniqueSlugError path so that slug-collision
errors on the final attempt exit the loop and allow the fallback AppError to be
thrown, while still rethrowing non-slug-related errors immediately.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3ba5efa0-bd63-43c3-89c6-0e86e4972dc4
📒 Files selected for processing (5)
prisma/migrations/20260623000000_init/migration.sqlprisma/schema.prismasrc/services/invoice.services.tssrc/utils/invoice.validation.tstests/integration/invoice.routes.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- prisma/migrations/20260623000000_init/migration.sql
- prisma/schema.prisma
🛑 Comments failed to post (1)
src/services/invoice.services.ts (1)
58-82: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Unreachable fallback: the 500 error on retry exhaustion is never thrown.
On the final iteration (
attempt === SLUG_MAX_RETRIES - 1), the guardattempt < SLUG_MAX_RETRIES - 1is false, so a slug collision rethrows the rawP2002error instead of falling through to the friendlyAppError(500, 'Failed to generate a unique payment slug')at Line 81. Thatthrowis therefore unreachable, and callers receive a raw Prisma error after exhausting retries.🐛 Proposed fix
} catch (error) { - if (isUniqueSlugError(error) && attempt < SLUG_MAX_RETRIES - 1) { - continue; - } - throw error; + if (!isUniqueSlugError(error)) { + throw error; + } }This lets a persistent slug collision exit the loop and reach the
AppError(500)fallback while still rethrowing any non-collision error immediately.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.for (let attempt = 0; attempt < SLUG_MAX_RETRIES; attempt++) { try { const invoice = await prisma.invoice.create({ data: { merchantId, description: data.description.trim(), amount, token: data.token.trim(), email: data.payerEmail?.trim() ?? null, expiresAt, status, paymentSlug: generatePaymentSlug(), }, }); return sanitizeInvoice(invoice); } catch (error) { if (!isUniqueSlugError(error)) { throw error; } } } throw new AppError(500, 'Failed to generate a unique payment slug'); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/invoice.services.ts` around lines 58 - 82, The retry loop in create invoice payment slug generation currently rethrows the raw Prisma unique constraint error on the last collision, making the AppError(500, 'Failed to generate a unique payment slug') fallback unreachable. Update the catch logic in src/services/invoice.services.ts around the prisma.invoice.create and isUniqueSlugError path so that slug-collision errors on the final attempt exit the loop and allow the fallback AppError to be thrown, while still rethrowing non-slug-related errors immediately.
codebestia
left a comment
There was a problem hiding this comment.
LGTM!
Nice Implementation.
Thank you for your contribution.
Summary
Merchant— addsaddress @unique; full profile fields already present are retainedInvoiceStatusenum — renamesStatus→InvoiceStatusand addsEXPIREDto match the smart contract's status namingInvoice— addspaymentSlug String @uniquefor payment-link routingApiKey— new model; storeskeyHashonly (SHA-256 of the raw key), never plaintext; supports optionalname,expiresAt, and soft-revocation viarevokedAtRefreshToken— renamesMerchantSessiontoRefreshToken; table purpose is now explicitAdmin— retained withaddress @uniqueadded; admin auth is a separate concern outside merchant flows20260623000000_initbaseline generated viaprisma migrate diff --from-emptyHow to apply on a fresh database
Test plan
Closes #5
Summary by CodeRabbit
registeredflag.