Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b713e48
fix(booking): every status history row names its appointment, and cre…
teetangh Sep 3, 2026
4d161dd
feat(booking): top up an allocation instead of re-planning it, and le…
teetangh Sep 3, 2026
aa2ce08
fix(booking): free the last tentative holds by status, and route the …
teetangh Sep 3, 2026
e1766fa
feat(payments): scoped rate cards reach settlement behind a flag that…
teetangh Sep 3, 2026
0f13b31
fix(payments): the checkout consent gate reads through the transactio…
teetangh Sep 4, 2026
6ae984d
docs(finance): 2026-09-03 verdict record, ADR 26, compliance-doc alig…
teetangh Sep 4, 2026
b9e4213
chore(cron): a Netlify scheduled ticker drives the sub-hourly money s…
teetangh Sep 4, 2026
369f24b
fix(payments): the leg-sum identity excludes referral credits, matchi…
teetangh Sep 4, 2026
e5e3b17
feat(payments): every consumer supply gets a statutory tax invoice, a…
teetangh Sep 4, 2026
6e80958
fix(payments): fence Stripe behind STRIPE_ENABLED and give it the gua…
teetangh Sep 4, 2026
a6bbb1e
feat(compliance): org-rail TDS reaches the Form 140 draft, and the CA…
teetangh Sep 4, 2026
0c85b7f
fix(payments): refunds and disputes find their Payment by the gateway…
teetangh Sep 4, 2026
ee22ca3
fix(payments): settlement is INR at the gateway boundary, checkout sa…
teetangh Sep 4, 2026
27fba2a
fix(payouts): the stuck-payout retry moves status through a CAS, a ra…
teetangh Sep 5, 2026
88226b1
chore(ci): every scheduled workflow carries a concurrency group (#1448)
teetangh Sep 5, 2026
a0dcacf
fix(razorpay): productionization pass — terminal payout statuses, a b…
teetangh Sep 5, 2026
9c19311
fix(enterprise): a wallet-funded overage is collected by the wallet d…
teetangh Sep 5, 2026
8a876b6
fix(booking-ui): held bookings show their deadline, and a frozen or d…
teetangh Sep 5, 2026
c1f9752
fix(enterprise): wallet credits and debits are null-safe on the cache…
teetangh Sep 5, 2026
3dd6a9f
fix(payments): empty gateway notes are omitted, and a buyer's own hol…
teetangh Sep 5, 2026
eebb19e
fix(pdf): the statutory documents create their elements with the Reac…
teetangh Sep 5, 2026
0cfb6c5
fix(payouts): the org payout journal clears the payable pre-withholdi…
teetangh Sep 5, 2026
1f32ba9
fix(payments): an overdrawn org wallet answers 402 with a top-up prom…
teetangh Sep 5, 2026
5434993
Merge branch 'prod' into release/dev-to-prod-2026-09-05
teetangh Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 8 additions & 1 deletion .claude/skills/booking-doctrine/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ edge, and narrowing is safer: automated completion from a Stream webhook passes
UNVERIFIED. As of wave 5 (#1322, ADR A12) each helper also appends one
`BookingStatusHistory` row in the same transaction, reading the from-status
before the CAS, so a lost race logs a stale from-status but never a wrong
state.
state. Wave 6 (#1333) widened that same pre-read to fetch the owning
appointment, so `appointmentId` is stamped without a caller supplying it, and
added `appendCreationHistory` — the one row that is not a transition, written
from the literal `"CREATED"` in the same transaction as the create, because a
booking that has never moved still needs a timeline. The three creation call
sites are `app/api/slots/request-for-approval` and the consultation and
subscription checkout handlers; the capture webhook's legacy creators do not
write it yet.

## 2. Nothing that a Payment points at is ever deleted

Expand Down
14 changes: 9 additions & 5 deletions .claude/skills/razorpay/references/refunds.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,15 @@ idempotencyKey: reserved.id
`createRazorpayRefund` sends the header only when the caller supplies a key. No key is
safer than a guessed one.

Two responses to expect. A **409** means a request with the same key is still in flight —
it is retryable, and `postRefund` retries once before giving up to the reconcile cron with
`REFUND_IN_FLIGHT`. The same key with a *different* payload returns `BAD_REQUEST`. The
`receipt` field also acts as a secondary idempotency key ("Duplicate receipt found for
this refund request").
Two responses to expect, and Razorpay answers both of them with a **409**. When another
request carrying the same key is still in flight, the description reads "still in
progress" and the conflict is retryable: `postRefund` retries once before giving up to the
reconcile cron with `REFUND_IN_FLIGHT`. When the same key is replayed with a *different*
payload, the description reads "Different request with the same idempotency key has
already been processed" and no amount of retrying will change the answer, so `postRefund`
throws it immediately as `REFUND_IDEMPOTENCY_KEY_REUSED` — a key collision is our bug, not
the gateway's. The `receipt` field also acts as a secondary idempotency key ("Duplicate
receipt found for this refund request").

Sources: <https://razorpay.com/docs/api/refunds/normal-refunds-idempotent/> ·
<https://razorpay.com/docs/api/refunds/instant-refunds-idempotent/> ·
Expand Down
35 changes: 35 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ NEXT_PUBLIC_RAZORPAY_KEY_ID=""
RAZORPAY_SECRET=""
# Webhook signing secret from the Razorpay dashboard (Settings > Webhooks)
RAZORPAY_WEBHOOK_SECRET=""
# The previous webhook signing secret, kept only for the window in which a
# rotated secret is being rolled out: the route (#1451) accepts a signature
# made with either value, so deliveries already in flight under the old secret
# are still verified instead of being rejected as forgeries. Leave it empty
# outside a rotation, and delete the old value once the dashboard has been
# switched over and no unverified deliveries remain.
RAZORPAY_WEBHOOK_SECRET_PREVIOUS=""

# #677 PM-1 — RazorpayX PRODUCTION payout credentials (live disbursements +
# payout webhook verification). Falls back to RAZORPAY_KEY_ID/RAZORPAY_SECRET
Expand All @@ -81,6 +88,18 @@ STRIPE_SECRET_KEY=""
NEXT_PUBLIC_STRIPE_KEY=""
STRIPE_WEBHOOK_SECRET=""

# #1351 — Stripe is a fenced contingency rail, not a live payment method:
# Razorpay takes every collection and Dodo Payments is the post-MVP
# international gateway. All three below are OPTIONAL and unset means "off".
# STRIPE_ENABLED gates the server (assertGatewayUsable); the NEXT_PUBLIC_ twin
# only gates the checkout button and is inlined at build time, so turning
# Stripe on needs both plus a redeploy. STRIPE_ALLOW_TEST_KEYS_IN_PRODUCTION
# downgrades the sk_test_-under-NODE_ENV=production throw to a loud log for the
# pre-launch window; delete it with the first live key.
STRIPE_ENABLED=""
NEXT_PUBLIC_STRIPE_ENABLED=""
STRIPE_ALLOW_TEST_KEYS_IN_PRODUCTION=""

# Stream API keys for real-time features
NEXT_PUBLIC_STREAM_API_KEY=""
STREAM_API_KEY=""
Expand Down Expand Up @@ -151,6 +170,14 @@ ORG_PAYOUT_ENCRYPTION_KEY=""
# address or the GSTIN's registered state changes; CA sign-off recommended.
SUPPLIER_STATE_CODE="KA"

# GST supplier identity for B2C tax invoices (ADR 26: the platform bills as
# principal). Leave empty until the real GSTIN is issued: mintConsumerInvoice
# then no-ops with a warning and the outward-register job heals the gap later.
PLATFORM_GSTIN=""
# Series prefix for consumer invoice / credit-note numbers (FAM-<FY>-<SEQ5>,
# FAM-CN-<FY>-<SEQ4>). Rule 46 caps the whole number at 16 characters.
PLATFORM_INVOICE_PREFIX="FAM"

# Enterprise — consolidated-invoice rollup cron flag.
# When "true", the monthly consolidated-invoice cron rolls up children's
# unpaid OrganizationInvoice rows into a single parent-org invoice on the
Expand All @@ -177,3 +204,11 @@ BETTERSTACK_INGEST_URL=""
# are NOT the production payout creds and do NOT flip ENABLE_LIVE_PAYOUTS.
RAZORPAYX_SANDBOX_KEY=""
RAZORPAYX_SANDBOX_SECRET=""

# Cron ticker (ADR 27). netlify/functions/cron-tick.mts POSTs the sub-hourly
# money sweeps under app/api/cleanup/* every 5 minutes with this bearer token;
# unset means the ticker logs and exits, and the routes reject every caller.
# Generate with: openssl rand -hex 32. Netlify production context only.
CRON_SECRET=""
# Optional: where the ticker POSTs. Defaults to the deploy URL Netlify sets.
CRON_TICK_BASE_URL=""
4 changes: 4 additions & 0 deletions .github/workflows/advance-program-cycles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
- cron: "15 2 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
advance-program-cycles:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/alert-dispute-deadlines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "2 * * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
alert-dispute-deadlines:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/alert-orphaned-payments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "30 */6 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
alert-orphaned-payments:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/archive-webhook-events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "25 0 * * 0"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
archive-webhook-events:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/auto-complete-appointments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
# cron-runtime-minutes: 6
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
auto-complete-appointments:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/auto-renew-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
- cron: "30 2 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
auto-renew-contracts:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/cascade-refund-earnings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ name: Cascade Refund to Earnings
on:
schedule:
# Run every 15 minutes
# ADR 22 — this schedule is an upper bound on frequency, not a delivered cadence; ADR 27 — the Netlify ticker (netlify/functions/cron-tick.mts) is what actually holds the cadence for the money sweeps.
- cron: "1-59/15 * * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
cascade-refund-earnings:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/cleanup-abandoned-org-top-ups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
- cron: "0 2 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
cleanup-abandoned-org-top-ups:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/cleanup-abandoned-payments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ name: Cleanup Abandoned Payments
on:
schedule:
# Run every 15 minutes
# ADR 22 — this schedule is an upper bound on frequency, not a delivered cadence; ADR 27 — the Netlify ticker (netlify/functions/cron-tick.mts) is what actually holds the cadence for the money sweeps.
- cron: "6-59/15 * * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
cleanup-abandoned-payments:
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/cleanup-auth-tokens.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ on:
- cron: "3 0 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
# A second workflow_dispatch while one is already pending would replace it
# under the default single-pending-run policy; queue: max keeps every
# trigger instead of silently dropping one.
queue: max

jobs:
cleanup-auth-tokens:
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/cleanup-empty-folders.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ on:
- cron: "30 3 * * *"
workflow_dispatch: # Allows manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
# A second workflow_dispatch while one is already pending would replace it
# under the default single-pending-run policy; queue: max keeps every
# trigger instead of silently dropping one.
queue: max

jobs:
cleanup:
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/cleanup-invalid-appointments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ on:
# cron-runtime-minutes: 5
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
# A second workflow_dispatch while one is already pending would replace it
# under the default single-pending-run policy; queue: max keeps every
# trigger instead of silently dropping one.
queue: max

jobs:
cleanup-invalid-appointments:
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/cleanup-old-stream-recordings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ on:
- cron: "0 3 * * *"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
# A second workflow_dispatch while one is already pending would replace it
# under the default single-pending-run policy; queue: max keeps every
# trigger instead of silently dropping one.
queue: max

jobs:
cleanup-old-stream-recordings:
runs-on: ubuntu-latest
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/cleanup-stale-invitations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ on:
- cron: "40 2 * * *"
workflow_dispatch: # allow manual trigger from the Actions tab

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
# A second workflow_dispatch while one is already pending would replace it
# under the default single-pending-run policy; queue: max keeps every
# trigger instead of silently dropping one.
queue: max

jobs:
cleanup-stale-invitations:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/cleanup-stale-pending-consultations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "37 * * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
cleanup-stale-pending-consultations:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/cleanup-tentative-slots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
# cron-runtime-minutes: 5
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
cleanup-tentative-slots:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/consent-retention-sweeper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
- cron: "0 21 * * 0"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
consent-retention-sweeper:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/cron-heartbeat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ on:
- cron: "40 4 * * *"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

# Read-only access to the Actions API; no repo write is needed or wanted.
permissions:
contents: read
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/databreach-deadline-alerts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
- cron: "27 * * * *"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
databreach-deadline-alerts:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/deactivate-expired-discounts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "15 0 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
deactivate-expired-discounts:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/detect-consultant-no-shows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "57 * * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
detect-consultant-no-shows:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/dispatch-outbound-webhooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ on:
schedule:
# Every minute. The worker is idempotent — a tick that overlaps an
# in-flight tick observes IN_FLIGHT rows and skips them.
# ADR 22 — this schedule is an upper bound on frequency, not a delivered cadence; ADR 27 — the Netlify ticker (netlify/functions/cron-tick.mts) is what actually holds the cadence for the money sweeps.
- cron: "* * * * *"
workflow_dispatch: # Allow manual triggering for debugging

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
dispatch-outbound-webhooks:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/dunning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
- cron: "30 23 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
dunning:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/expire-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
- cron: "10 3 * * *"
workflow_dispatch: # Allow manual triggering

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
expire-contracts:
runs-on: ubuntu-latest
Expand Down
Loading
Loading