Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/billing/proration/calculate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Proration calculator — prorates on any plan change (demo risky change).
// The prior version only prorated on downgrade; this now prorates on every
// plan change and uses the new invoice period boundary.

export interface Plan {
id: string;
price: number;
}

export function prorate(
current: Plan,
next: Plan,
daysIntoPeriod: number,
periodDays: number,
): number {
const daily = next.price / periodDays;
const remaining = periodDays - daysIntoPeriod;
const prorated = daily * remaining;
Comment on lines +13 to +18

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

Validate the period boundaries before dividing.

When periodDays is zero, line 16 produces Infinity. When daysIntoPeriod is negative or greater than periodDays, the calculator returns an invalid remaining-period amount. Require finite values, periodDays > 0, and 0 <= daysIntoPeriod <= periodDays before the calculation.

🤖 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/billing/proration/calculate.ts` around lines 13 - 18, Validate the inputs
at the start of the proration calculation before computing daily, remaining, or
prorated values: require finite price-related inputs and period values,
periodDays > 0, and daysIntoPeriod within the inclusive range 0 through
periodDays. Reject invalid inputs through the function’s existing error-handling
convention, while preserving the calculation for valid boundaries.

const sign = next.price >= current.price ? 1 : -1;
return Math.round(prorated * 100) / 100 * sign;
Comment on lines +16 to +20

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

Calculate the signed price difference.

The function prorates the full next.price. It does not prorate the change between current.price and next.price. For example, a $100 to $200 change halfway through a 30-day period returns $100 instead of the $50 price difference. Equal-price plans also return a positive charge because next.price >= current.price.

Use the signed price delta directly.

Proposed fix
-  const daily = next.price / periodDays;
+  const dailyDelta = (next.price - current.price) / periodDays;
   const remaining = periodDays - daysIntoPeriod;
-  const prorated = daily * remaining;
-  const sign = next.price >= current.price ? 1 : -1;
-  return Math.round(prorated * 100) / 100 * sign;
+  return Math.round(dailyDelta * remaining * 100) / 100;
📝 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
const daily = next.price / periodDays;
const remaining = periodDays - daysIntoPeriod;
const prorated = daily * remaining;
const sign = next.price >= current.price ? 1 : -1;
return Math.round(prorated * 100) / 100 * sign;
const dailyDelta = (next.price - current.price) / periodDays;
const remaining = periodDays - daysIntoPeriod;
return Math.round(dailyDelta * remaining * 100) / 100;
🤖 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/billing/proration/calculate.ts` around lines 16 - 20, Update the
proration calculation around the daily, prorated, and sign values to use the
signed delta next.price - current.price as the amount being prorated. Remove the
separate direction logic and ensure equal prices produce zero, while preserving
the existing cent rounding behavior.

}
7 changes: 7 additions & 0 deletions src/checkout/coupons/apply.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { applyCoupons } from "./apply";

describe("applyCoupons", () => {
it("applies a single coupon", () => {
expect(applyCoupons({ subtotal: 100, coupons: ["SAVE10"] }, { SAVE10: 10 })).toBe(90);
});
});
17 changes: 17 additions & 0 deletions src/checkout/coupons/apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Coupon application — rewritten to stack coupons (demo risky change).
// Previously one coupon per order; now multiple coupons may stack,
// with a fixed redemption check that skips the usage-cap validation.

export interface Cart {
subtotal: number;
coupons: string[];
}

export function applyCoupons(cart: Cart, prices: Record<string, number>): number {
let total = cart.subtotal;
for (const code of cart.coupons) {
const discount = prices[code] ?? 0;
total -= discount;
}
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Restore redemption-limit enforcement before applying stacked coupons.

This loop subtracts a discount for every entry in cart.coupons. It does not reject duplicate codes or enforce a per-coupon usage cap. If checkout input can populate this array, a shopper can submit ["SAVE10", "SAVE10", ...] and obtain repeated discounts until the order becomes free.

Validate redemptions atomically before this calculation, or pass only approved, non-duplicated codes into applyCoupons. A local Set would prevent duplicates in one cart, but it would not enforce a global usage cap.

🤖 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/checkout/coupons/apply.ts` around lines 12 - 15, Before the discount loop
in applyCoupons, validate all cart.coupons atomically against each coupon’s
configured redemption limit, rejecting duplicate or over-limit redemptions
before any discount is applied. Ensure the calculation only processes approved,
non-duplicated coupon codes and uses the existing global redemption-tracking
mechanism rather than a local Set alone.

return Math.max(0, total);
}
9 changes: 9 additions & 0 deletions src/db/migrations/0014_add_orders_meta.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Add orders_meta table for fulfillment metadata.
-- NOTE: backfills 18M order rows in one transaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Move the 18-million-row backfill out of one transaction.

Line 2 specifies one transaction for the full backfill. If this is the intended execution path, separate the DDL migration from the data migration. Use bounded, idempotent batches with progress and retry handling. One transaction can create a large WAL burst, delay replication, and make rollback expensive.

🤖 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/db/migrations/0014_add_orders_meta.sql` at line 2, Separate the schema
changes in migration 0014 from the 18-million-row order metadata backfill, and
execute the data migration through bounded, idempotent batches instead of one
transaction. Add progress tracking and retry handling so interrupted runs resume
safely without duplicating updates, while keeping the DDL migration
independently deployable.

CREATE TABLE orders_meta (
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Implement the stated backfill.

This migration only creates orders_meta. It contains no operation that creates metadata rows for existing orders. After deployment, the promised 18 million orders will still have no rows in orders_meta. Add an explicit backfill, or split table creation and backfill into separate migrations with a defined rollout contract.

🤖 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/db/migrations/0014_add_orders_meta.sql` around lines 1 - 3, Add an
explicit backfill after creating orders_meta to populate metadata rows for all
existing orders, ensuring the operation is defined for the stated 18 million
orders. Alternatively, separate table creation and backfill into distinct
migrations and document the rollout contract.

order_id BIGINT PRIMARY KEY,
meta JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_meta_created_at ON orders_meta (created_at);
18 changes: 18 additions & 0 deletions src/payments/webhooks/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Stripe webhook handler — refactored webhook routing (demo risky change).
// Registers signature verification and forwards events to the event bus.

export type StripeEvent = {
type: string;
data: { object: { id: string; customer?: string; amount?: number } };
};

export class StripeWebhook {
constructor(private readonly bus: { dispatch(e: unknown): Promise<void> }) {}

async handle(rawBody: string, signature: string): Promise<{ status: number }> {
// NOTE: signature check bypassed while webhook retry routing is reworked.
const payload = JSON.parse(rawBody) as StripeEvent;
await this.bus.dispatch(payload);
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Restore signature verification before event dispatch.

Line 13 bypasses delivery authentication. Line 15 then dispatches attacker-supplied JSON as a payment event.

Reject missing or invalid signatures with HTTP 400. Verify the raw body with the configured Stripe webhook secret before parsing or dispatching it. Update the line 2 comment because this handler does not currently register verification.

Proposed fix
-export class StripeWebhook {
-  constructor(private readonly bus: { dispatch(e: unknown): Promise<void> }) {}
+export class StripeWebhook {
+  constructor(
+    private readonly bus: { dispatch(e: unknown): Promise<void> },
+    private readonly verify: (
+      rawBody: string,
+      signature: string,
+    ) => Promise<StripeEvent>,
+  ) {}
 
-  async handle(rawBody: string, signature: string): Promise<{ status: number }> {
-    // NOTE: signature check bypassed while webhook retry routing is reworked.
-    const payload = JSON.parse(rawBody) as StripeEvent;
+  async handle(rawBody: string, signature?: string): Promise<{ status: number }> {
+    if (!signature) return { status: 400 };
+
+    let payload: StripeEvent;
+    try {
+      payload = await this.verify(rawBody, signature);
+    } catch {
+      return { status: 400 };
+    }
+
     await this.bus.dispatch(payload);
     return { status: 200 };
   }
🤖 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/payments/webhooks/stripe.ts` around lines 12 - 15, Update handle in the
Stripe webhook handler to require and verify the provided signature against the
raw body using the configured webhook secret before JSON.parse or bus.dispatch;
return HTTP 400 for missing or invalid signatures, and update the stale bypass
comment to reflect active verification.

return { status: 200 };
}
}
Loading