Skip to content

Conversation

bassgeta
Copy link
Contributor

@bassgeta bassgeta commented Oct 2, 2025

Changes

  • added new clientPayment entity that stores all payments made with a client id
  • modified the webhook route to store a client payment instead of a general request if clientId is present on the webhook body
  • implemented an overview of all payments for the manage view
  • implemented an overview of all the user's "receipts" -> payments made via our API using clientIds

Testing

It's going to be the easiest to test this alongside this PR running on localhost:3001 and having easy invoice on 3000.

  1. Create the default client ID and copy the value. Go to RN checkout and add something to your cart, get to the payment step.
  2. Enter your client ID here, go over the payment flow. NOTE: Use the same email for customer info as the email with which you're logged into Easy Invoice!!!
image image 3. Once a payment is done and you are on the success step, go over to easy invoice's sales table and refresh it. Verify that you get the sales in the table and you can filter (although we only have 1 merchant so far...) image 4. Also verify that you get this payment in your receipts tab image

Resolves #152

@bassgeta bassgeta self-assigned this Oct 2, 2025
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Implements client payment ingestion via webhook, adds DB schema and TRPC endpoints for client payments and user receipts, and introduces UI pages/components for Sales (merchant view) and Receipts (user view) with filtering, pagination, and error/empty states. Navigation updated to include a Receipts tab.

Changes

Cohort / File(s) Summary of changes
Webhook: client payment handling
src/app/api/webhook/route.ts
Adds addClientPayment to persist client-based payments on webhook; guards duplicates; joins with ecommerceClientTable; updates POST flow to branch on body.clientId; imports new tables.
DB schema and relations
src/server/db/schema.ts
Introduces clientPaymentTable, relations to user and ecommerceClient, unique index on ecommerceClientTable (rnClientId with user/domain), exports ClientPayment type and relations.
TRPC router: payments and receipts
src/server/routers/ecommerce.ts
Adds protected procedures: getAllClientPayments (by user, with client join) and getAllUserReceipts (by customer email in JSON, with client join).
Shared types
src/lib/types/index.ts
Exports ClientPaymentWithEcommerceClient inferred from ecommerceRouter.getAllClientPayments.
Ecommerce Sales UI and page
src/components/ecommerce/sales/index.tsx, src/components/ecommerce/sales/blocks/client-payments-table.tsx, src/app/ecommerce/sales/page.tsx
New Sales page gated by session; fetches getAllClientPayments and renders EcommerceSales with ClientPaymentsTable, including client filter, pagination, and row details.
Dashboard Receipts UI and page
src/components/dashboard/receipts.tsx, src/app/dashboard/receipts/page.tsx
New Receipts page gated by session; fetches getAllUserReceipts; DashboardReceipts component with merchant filter, pagination, error/empty states.
Dashboard navigation
src/components/dashboard-navigation.tsx
Adds “Receipts” tab, updates active-tab detection, expands grid to 4 columns.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor RequestNetwork as Request Network Webhook
  participant API as API /webhook (POST)
  participant DB as Database
  Note over RequestNetwork,API: payment.confirmed (with clientId)

  RequestNetwork->>API: POST /api/webhook { clientId, ... }
  alt clientId present
    API->>DB: Check duplicate in clientPaymentTable
    DB-->>API: Duplicate? (no)
    API->>DB: Lookup ecommerceClient by rnClientId + user/domain
    DB-->>API: ecommerceClient row or null
    alt client found
      API->>DB: Insert clientPaymentTable (mapped fields)
      DB-->>API: Inserted row
      API-->>RequestNetwork: 200 OK
    else client not found
      API-->>RequestNetwork: 400/500 Error
    end
  else no clientId
    Note right of API: Execute existing flows (e.g., recurring)
    API-->>RequestNetwork: Response
  end
Loading
sequenceDiagram
  autonumber
  actor User as Merchant User
  participant Page as /ecommerce/sales
  participant TRPC as ecommerceRouter.getAllClientPayments
  participant DB as Database

  User->>Page: Navigate
  Page->>TRPC: Query payments (session user)
  TRPC->>DB: SELECT clientPayment + join ecommerceClient
  DB-->>TRPC: Rows
  TRPC-->>Page: Data
  Page->>Page: Render ClientPaymentsTable (filter/pagination)
Loading
sequenceDiagram
  autonumber
  actor EndUser as End User
  participant Page as /dashboard/receipts
  participant TRPC as ecommerceRouter.getAllUserReceipts
  participant DB as Database

  EndUser->>Page: Navigate (requires session)
  Page->>TRPC: Query receipts (by user email in customerInfo JSON)
  TRPC->>DB: SELECT clientPayment WHERE customerInfo.email = user.email
  DB-->>TRPC: Rows
  TRPC-->>Page: Data
  Page->>Page: Render DashboardReceipts (filter/pagination)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rodrigopavezi
  • aimensahnoun

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the addition of both ecommerce sales and user receipts features, directly reflecting the main changes in the pull request without extraneous details or ambiguity.
Linked Issues Check ✅ Passed The implementation fulfills all primary coding requirements from issue #152 by adding the Sales tab with a client-filterable payments table, the Receipts dashboard tab for user payments, backend webhook handling, database schema updates, new API procedures, and corresponding UI components.
Out of Scope Changes Check ✅ Passed All modifications directly relate to integrating checkout payments into EasyInvoice and implementing the specified sales and receipts functionality, with no unrelated or extraneous changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

@bassgeta bassgeta force-pushed the feat/152-receipts-and-sales branch from 82c9b3e to 77a7575 Compare October 3, 2025 08:41
@bassgeta bassgeta marked this pull request as ready for review October 3, 2025 11:30
@bassgeta
Copy link
Contributor Author

bassgeta commented Oct 3, 2025

@CodeRabbit review

Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/components/dashboard/receipts.tsx (1)

94-104: Clamp the active page when data shrinks

When a refetch returns fewer receipts, currentPage can stay above the new totalPages, yielding an empty table even though data exists. Clamp it whenever the filtered length changes.

-const totalPages = Math.ceil(filteredReceipts.length / ITEMS_PER_PAGE);
+const totalPages = Math.max(
+  1,
+  Math.ceil(filteredReceipts.length / ITEMS_PER_PAGE),
+);
+if (currentPage > totalPages) {
+  setCurrentPage(totalPages);
+}
src/components/ecommerce/sales/blocks/client-payments-table.tsx (1)

174-183: Clamp pagination after filtered data updates

Just like the receipts table, a refetch that reduces filteredPayments.length can leave currentPage > totalPages, yielding an empty UI despite available rows. Clamp or reset the page when totalPages drops.

-const totalPages = Math.ceil(filteredPayments.length / ITEMS_PER_PAGE);
+const totalPages = Math.max(
+  1,
+  Math.ceil(filteredPayments.length / ITEMS_PER_PAGE),
+);
+if (currentPage > totalPages) {
+  setCurrentPage(totalPages);
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 744e1e0 and a41867b.

📒 Files selected for processing (10)
  • src/app/api/webhook/route.ts (3 hunks)
  • src/app/dashboard/receipts/page.tsx (1 hunks)
  • src/app/ecommerce/sales/page.tsx (2 hunks)
  • src/components/dashboard-navigation.tsx (2 hunks)
  • src/components/dashboard/receipts.tsx (1 hunks)
  • src/components/ecommerce/sales/blocks/client-payments-table.tsx (1 hunks)
  • src/components/ecommerce/sales/index.tsx (1 hunks)
  • src/lib/types/index.ts (2 hunks)
  • src/server/db/schema.ts (5 hunks)
  • src/server/routers/ecommerce.ts (2 hunks)

Comment on lines 19 to 67
async function addClientPayment(webhookBody: any) {
await db.transaction(async (tx) => {
const existingPayment = await tx
.select()
.from(clientPaymentTable)
.where(
and(
eq(clientPaymentTable.txHash, webhookBody.txHash),
eq(clientPaymentTable.requestId, webhookBody.requestId),
),
)
.limit(1);

if (existingPayment.length > 0) {
console.warn(
`Duplicate payment detected for txHash: ${webhookBody.txHash} and requestId: ${webhookBody.requestId}`,
);
return;
}

const ecommerceClient = await tx
.select()
.from(ecommerceClientTable)
.where(eq(ecommerceClientTable.rnClientId, webhookBody.clientId))
.limit(1);

if (!ecommerceClient.length) {
throw new ResourceNotFoundError(
`No ecommerce client found with client ID: ${webhookBody.clientId}`,
);
}

const client = ecommerceClient[0];

await tx.insert(clientPaymentTable).values({
id: ulid(),
userId: client.userId,
ecommerceClientId: client.id,
requestId: webhookBody.requestId,
invoiceCurrency: webhookBody.currency,
paymentCurrency: webhookBody.paymentCurrency,
txHash: webhookBody.txHash,
network: webhookBody.network,
amount: webhookBody.amount,
customerInfo: webhookBody.customerInfo || null,
reference: webhookBody.reference || null,
origin: webhookBody.origin,
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make duplicate protection atomic

The “select then insert” guard still lets two concurrent webhook deliveries race past the duplicate check and both insert, so we can double-count the same payment (Request webhooks routinely retry on network hiccups). We need the database to enforce idempotency. Please rely on an atomic insert with ON CONFLICT DO NOTHING (or a unique constraint + error handling) instead of the manual pre-check.

-    const existingPayment = await tx
-      .select()
-      .from(clientPaymentTable)
-      .where(
-        and(
-          eq(clientPaymentTable.txHash, webhookBody.txHash),
-          eq(clientPaymentTable.requestId, webhookBody.requestId),
-        ),
-      )
-      .limit(1);
-
-    if (existingPayment.length > 0) {
-      console.warn(
-        `Duplicate payment detected for txHash: ${webhookBody.txHash} and requestId: ${webhookBody.requestId}`,
-      );
-      return;
-    }
-
-    const ecommerceClient = await tx
+    const ecommerceClient = await tx
       .select()
       .from(ecommerceClientTable)
       .where(eq(ecommerceClientTable.rnClientId, webhookBody.clientId))
       .limit(1);
 
@@
-    await tx.insert(clientPaymentTable).values({
-      id: ulid(),
-      userId: client.userId,
-      ecommerceClientId: client.id,
-      requestId: webhookBody.requestId,
-      invoiceCurrency: webhookBody.currency,
-      paymentCurrency: webhookBody.paymentCurrency,
-      txHash: webhookBody.txHash,
-      network: webhookBody.network,
-      amount: webhookBody.amount,
-      customerInfo: webhookBody.customerInfo || null,
-      reference: webhookBody.reference || null,
-      origin: webhookBody.origin,
-    });
+    const inserted = await tx
+      .insert(clientPaymentTable)
+      .values({
+        id: ulid(),
+        userId: client.userId,
+        ecommerceClientId: client.id,
+        requestId: webhookBody.requestId,
+        invoiceCurrency: webhookBody.currency,
+        paymentCurrency: webhookBody.paymentCurrency,
+        txHash: webhookBody.txHash,
+        network: webhookBody.network,
+        amount: webhookBody.amount,
+        customerInfo: webhookBody.customerInfo || null,
+        reference: webhookBody.reference || null,
+        origin: webhookBody.origin,
+      })
+      .onConflictDoNothing({
+        target: [clientPaymentTable.txHash, clientPaymentTable.requestId],
+      })
+      .returning({ id: clientPaymentTable.id });
+
+    if (!inserted.length) {
+      console.warn(
+        `Duplicate payment detected for txHash: ${webhookBody.txHash} and requestId: ${webhookBody.requestId}`,
+      );
+      return;
+    }

Comment on lines 230 to 243
{paginatedPayments.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="p-0">
<EmptyState
icon={<CreditCard className="h-6 w-6 text-zinc-600" />}
title="No client payments"
subtitle={
activeClientId
? "No payments found for the selected client"
: "No payments received yet"
}
/>
</TableCell>
</TableRow>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Match empty-state colSpan to the header

The header defines 10 columns, but the empty-state row spans only 9, leaving an extra blank column and misaligning borders. Make the colSpan 10.

-                  <TableCell colSpan={9} className="p-0">
+                  <TableCell colSpan={10} className="p-0">
📝 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.

Suggested change
{paginatedPayments.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="p-0">
<EmptyState
icon={<CreditCard className="h-6 w-6 text-zinc-600" />}
title="No client payments"
subtitle={
activeClientId
? "No payments found for the selected client"
: "No payments received yet"
}
/>
</TableCell>
</TableRow>
{paginatedPayments.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="p-0">
<EmptyState
icon={<CreditCard className="h-6 w-6 text-zinc-600" />}
title="No client payments"
subtitle={
activeClientId
? "No payments found for the selected client"
: "No payments received yet"
}
/>
</TableCell>
</TableRow>
🤖 Prompt for AI Agents
In src/components/ecommerce/sales/blocks/client-payments-table.tsx around lines
230 to 243, the EmptyState TableCell uses colSpan={9} but the table header
defines 10 columns; update the TableCell colSpan to 10 so the empty-state row
spans all header columns and fixes the border/alignment.

Comment on lines 180 to 213
export const clientPaymentTable = createTable("client_payment", {
id: text().primaryKey().notNull(),
userId: text()
.notNull()
.references(() => userTable.id, {
onDelete: "cascade",
}),
requestId: text().notNull(),
ecommerceClientId: text()
.notNull()
.references(() => ecommerceClientTable.id, {
onDelete: "cascade",
}),
invoiceCurrency: text().notNull(),
paymentCurrency: text().notNull(),
txHash: text().notNull(),
network: text().notNull(),
amount: text().notNull(),
customerInfo: json().$type<{
firstName?: string;
lastName?: string;
email?: string;
address?: {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
}>(),
reference: text(),
origin: text(),
createdAt: timestamp("created_at").defaultNow(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix foreign key reference order to avoid runtime ReferenceError

references(() => ecommerceClientTable.id) is evaluated while defining clientPaymentTable, but ecommerceClientTable is still in the temporal dead zone. This throws ReferenceError: Cannot access 'ecommerceClientTable' before initialization, blocking the app at startup. Move the entire clientPaymentTable definition (and its relations export) below the ecommerceClientTable declaration so the referenced table is initialized first.

-export const clientPaymentTable = createTable("client_payment", {
-  ...
-});
-
 export const ecommerceClientTable = createTable(
   "ecommerce_client",
   {
     ...
   },
 );
 
+export const clientPaymentTable = createTable("client_payment", {
+  ...
+});
📝 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.

Suggested change
export const clientPaymentTable = createTable("client_payment", {
id: text().primaryKey().notNull(),
userId: text()
.notNull()
.references(() => userTable.id, {
onDelete: "cascade",
}),
requestId: text().notNull(),
ecommerceClientId: text()
.notNull()
.references(() => ecommerceClientTable.id, {
onDelete: "cascade",
}),
invoiceCurrency: text().notNull(),
paymentCurrency: text().notNull(),
txHash: text().notNull(),
network: text().notNull(),
amount: text().notNull(),
customerInfo: json().$type<{
firstName?: string;
lastName?: string;
email?: string;
address?: {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
}>(),
reference: text(),
origin: text(),
createdAt: timestamp("created_at").defaultNow(),
});
export const ecommerceClientTable = createTable(
"ecommerce_client",
{
...
},
);
export const clientPaymentTable = createTable("client_payment", {
id: text().primaryKey().notNull(),
userId: text()
.notNull()
.references(() => userTable.id, {
onDelete: "cascade",
}),
requestId: text().notNull(),
ecommerceClientId: text()
.notNull()
.references(() => ecommerceClientTable.id, {
onDelete: "cascade",
}),
invoiceCurrency: text().notNull(),
paymentCurrency: text().notNull(),
txHash: text().notNull(),
network: text().notNull(),
amount: text().notNull(),
customerInfo: json().$type<{
firstName?: string;
lastName?: string;
email?: string;
address?: {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
}>(),
reference: text(),
origin: text(),
createdAt: timestamp("created_at").defaultNow(),
});
🤖 Prompt for AI Agents
In src/server/db/schema.ts around lines 180 to 213, the clientPaymentTable is
defined before ecommerceClientTable which causes a ReferenceError when its
foreign key reference is evaluated; move the entire clientPaymentTable
definition (and any export or relations that reference it) to a position after
the ecommerceClientTable declaration so the referenced table is initialized
first, and ensure exports that expose relations are adjusted accordingly to
preserve load order.

@bassgeta bassgeta merged commit e6d0631 into feat/151-client-id-management Oct 3, 2025
5 checks passed
@bassgeta bassgeta deleted the feat/152-receipts-and-sales branch October 3, 2025 11:47
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.

1 participant