Skip to content

feat: redesign Prisma schema for full domain model#18

Merged
codebestia merged 9 commits into
ShadeProtocol:mainfrom
G-ELM:schema-redesign
Jun 24, 2026
Merged

feat: redesign Prisma schema for full domain model#18
codebestia merged 9 commits into
ShadeProtocol:mainfrom
G-ELM:schema-redesign

Conversation

@G-ELM

@G-ELM G-ELM commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Merchant — adds address @unique; full profile fields already present are retained
  • InvoiceStatus enum — renames StatusInvoiceStatus and adds EXPIRED to match the smart contract's status naming
  • Invoice — adds paymentSlug String @unique for payment-link routing
  • ApiKey — new model; stores keyHash only (SHA-256 of the raw key), never plaintext; supports optional name, expiresAt, and soft-revocation via revokedAt
  • RefreshToken — renames MerchantSession to RefreshToken; table purpose is now explicit
  • Admin — retained with address @unique added; admin auth is a separate concern outside merchant flows
  • Migration — old draft migration replaced with a single clean 20260623000000_init baseline generated via prisma migrate diff --from-empty

How to apply on a fresh database

DATABASE_URL=<your-url> npx prisma migrate reset --force
DATABASE_URL=<your-url> npx prisma migrate dev

Test plan

  • prisma validate passes — confirmed ✅
  • prisma generate completes without errors — confirmed ✅
  • tsc --noEmit reports zero errors in src/ — confirmed ✅
  • All 22 unit tests pass (npm test -- --testPathPatterns=unit) — confirmed ✅
  • prisma migrate dev runs without error on a fresh PostgreSQL database

Closes #5

Summary by CodeRabbit

  • New Features
    • Added/expanded support for refresh tokens, API keys, subscriptions, transactions, token analytics, and bridge payments.
    • Redefined invoicing with new invoice statuses, pricing modes, fiat fields, payment/refund tracking, and globally unique payment slugs.
    • Expanded merchant profile/auth relationships (including webhook/account fields).
  • Bug Fixes
    • Merchant authentication now uses refresh tokens to resolve the active session.
    • Wallet registration status now reflects the merchant’s registered flag.
    • Updated invoice payer contact field naming and status typing/validation to stay consistent.
  • Tests
    • Updated auth/invoice/registration fixtures and added unit coverage for analytics schema behavior.

G-ELM added 3 commits June 23, 2026 19:05
- Replace prisma.merchantSession with prisma.refreshToken following
  MerchantSession → RefreshToken rename
- Use merchant.registered instead of firstName !== null to determine
  isRegistered in authenticateWallet response
@codebestia

Copy link
Copy Markdown
Contributor

Hello @G-ELM
Please fix the CI issues.
Thanks

@codebestia codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@G-ELM

G-ELM commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

GM @codebestia
I'm working on it.
Thank you for the review.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7dce8950-cf75-422f-a6b4-01cd3d88c88c

📥 Commits

Reviewing files that changed from the base of the PR and between 67457a2 and eee11d2.

📒 Files selected for processing (3)
  • tests/unit/analytics.schema.test.ts
  • tests/unit/invoice.schema.test.ts
  • tests/unit/subscription.schema.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/invoice.schema.test.ts
  • tests/unit/subscription.schema.test.ts
  • tests/unit/analytics.schema.test.ts

📝 Walkthrough

Walkthrough

The 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.

Changes

Schema redesign and auth migration

Layer / File(s) Summary
Merchant, admin, and Prisma config
prisma/schema.prisma
Adds Prisma generator and PostgreSQL datasource blocks, makes Admin.address unique, and reshapes Merchant with unique address plus optional account and webhook fields.
Auth token models and refresh-token flow
prisma/schema.prisma, src/middlewares/auth.middleware.ts, src/services/auth.services.ts, tests/integration/auth.routes.test.ts, tests/integration/merchant.register.test.ts, tests/unit/auth.services.test.ts, tests/integration/invoice.routes.test.ts
Introduces RefreshToken and ApiKey, keeps AuthNonce as the merchant-linked nonce model, switches auth lookups and issuance to RefreshToken, and expands mocked merchant records for the new fields.
Invoice, pricing, and bridge payments
prisma/schema.prisma, src/services/invoice.services.ts, src/utils/invoice.validation.ts, tests/unit/invoice.schema.test.ts
Replaces Status with InvoiceStatus and InvoicePricingMode, redefines Invoice with payment slug uniqueness, pricing fields, amount tracking, and bridgePayments, and updates invoice service, validation, and tests to match.
Subscriptions, transactions, and analytics
prisma/schema.prisma, tests/unit/analytics.schema.test.ts, tests/unit/subscription.schema.test.ts
Adds MerchantAnalytics, SubscriptionPlan, Subscription, SubscriptionStatus, Transaction, TransactionType, and TokenAnalytics with their relation and uniqueness fields, along with unit coverage for their schema behavior.
Init migration SQL
prisma/migrations/20260623000000_init/migration.sql
Creates the public schema, defines enums, creates all tables, adds unique indexes, and wires the foreign key constraints for the expanded domain.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hop through schemas, bright and new,
Refresh tokens glimmer, invoices too.
Bridges, plans, and analytics hum,
In the burrow, fresh migrations come.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR adds analytics, subscriptions, transactions, and bridge-payment models/tests that are not requested in #5. Split the analytics/subscription/bridge additions into separate PRs, or add linked issues that explicitly scope them.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main Prisma schema redesign.
Linked Issues check ✅ Passed The schema, migration, and service changes satisfy #5's core requirements and constraints.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

tests/unit/analytics.schema.test.ts

Oops! Something went wrong! :(

ESLint: 9.24.0

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///eslint.config.js?mtime=1782322846774:1:24
at ModuleJob.run (node:internal/modules/esm/module_job:437:25)
at async node:internal/modules/esm/loader:639:26
at async dynamicImportConfig (/node_modules/eslint/lib/config/config-loader.js:181:17)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:271:9)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:584:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:765:23)
at async /node_modules/eslint/lib/eslint/eslint.js:746:6
at async Promise.all (index 0)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:743:19)

tests/unit/invoice.schema.test.ts

Oops! Something went wrong! :(

ESLint: 9.24.0

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///eslint.config.js?mtime=1782322846774:1:24
at ModuleJob.run (node:internal/modules/esm/module_job:437:25)
at async node:internal/modules/esm/loader:639:26
at async dynamicImportConfig (/node_modules/eslint/lib/config/config-loader.js:181:17)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:271:9)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:584:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:765:23)
at async /node_modules/eslint/lib/eslint/eslint.js:746:6
at async Promise.all (index 0)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:743:19)

tests/unit/subscription.schema.test.ts

Oops! Something went wrong! :(

ESLint: 9.24.0

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
at file:///eslint.config.js?mtime=1782322846774:1:24
at ModuleJob.run (node:internal/modules/esm/module_job:437:25)
at async node:internal/modules/esm/loader:639:26
at async dynamicImportConfig (/node_modules/eslint/lib/config/config-loader.js:181:17)
at async loadConfigFile (/node_modules/eslint/lib/config/config-loader.js:271:9)
at async ConfigLoader.calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:584:23)
at async #calculateConfigArray (/node_modules/eslint/lib/config/config-loader.js:765:23)
at async /node_modules/eslint/lib/eslint/eslint.js:746:6
at async Promise.all (index 0)
at async ESLint.lintFiles (/node_modules/eslint/lib/eslint/eslint.js:743:19)


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 503c35b and d864500.

📒 Files selected for processing (11)
  • prisma/migrations/20260621183614_demo/migration.sql
  • prisma/migrations/20260623000000_init/migration.sql
  • prisma/schema.prisma
  • src/middlewares/auth.middleware.ts
  • src/services/auth.services.ts
  • tests/integration/auth.routes.test.ts
  • tests/integration/merchant.register.test.ts
  • tests/unit/analytics.schema.test.ts
  • tests/unit/auth.services.test.ts
  • tests/unit/invoice.schema.test.ts
  • tests/unit/subscription.schema.test.ts
💤 Files with no reviewable changes (1)
  • prisma/migrations/20260621183614_demo/migration.sql

Comment thread prisma/schema.prisma
Comment on lines +88 to +97
// Matches the smart contract InvoiceStatus enum exactly.
enum InvoiceStatus {
PENDING
PAID
CANCELLED
REFUNDED
PARTIALLY_REFUNDED
PARTIALLY_PAID
DRAFT
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread prisma/schema.prisma Outdated
Comment thread prisma/schema.prisma Outdated
Comment on lines +52 to +282
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' });
});
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

@codebestia

Copy link
Copy Markdown
Contributor

@G-ELM
Please fix the CI and resolve the coderabbit reviews

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d864500 and 67457a2.

📒 Files selected for processing (5)
  • prisma/migrations/20260623000000_init/migration.sql
  • prisma/schema.prisma
  • src/services/invoice.services.ts
  • src/utils/invoice.validation.ts
  • tests/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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d864500 and 67457a2.

📒 Files selected for processing (5)
  • prisma/migrations/20260623000000_init/migration.sql
  • prisma/schema.prisma
  • src/services/invoice.services.ts
  • src/utils/invoice.validation.ts
  • tests/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 guard attempt < SLUG_MAX_RETRIES - 1 is false, so a slug collision rethrows the raw P2002 error instead of falling through to the friendly AppError(500, 'Failed to generate a unique payment slug') at Line 81. That throw is 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 codebestia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!
Nice Implementation.
Thank you for your contribution.

@codebestia codebestia merged commit c0e30a5 into ShadeProtocol:main Jun 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redesign Prisma Schema for Core Domain Models

2 participants