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
7 changes: 5 additions & 2 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,26 @@ Local `main` in the main clone is at `5693f7b`, an older Production commit that
| PR | Base ← Head | Head SHA | Checks on head | Latest CI run |
|---|---|---|---|---|
| #17 Production release: free-only MVP | `main` ← `launch-mvp` | `83a30a7` | `lint, unit, e2e` ✅ (push + PR) · Vercel ✅ · Vercel Preview Comments ✅ | [41564501659](https://github.com/Jakuraa/StrangersMeetup/actions/runs/41564501659) (push) success |
| #30 M4.3 UPI Security Remediation | `launch-mvp` ← `feature/m4-3-upi-security-remediation` | `e4babe6` | `lint, unit, e2e` ✅ | PR Review Baseline for M4.3.1 |
| (Remediation) M4.3.1 Review Remediation | `feature/m4-3-upi-security-remediation` ← `feature/m4-3-1-review-remediation` | (work branch) | `lint, unit, e2e` ✅ | Targets PR #30 branch for review remediation |

**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record.
**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #30 must not be merged automatically without owner review.

PR #23 is **merged** at `7c5079e` (P3 Location Intelligence + M2 Remediation).
PR #25 is **merged** at `83a30a7` (Owner Addendum Ops: dynamic DB block list, branch archiving, Phase 5.6/5.7 reports).
PR #26 is **merged** at `46b85c1` (M3 Deployment & Database Migration Architecture).
PR #27 is **merged** at `69cafae` (M4 Payment, Booking and Revenue Readiness Audit).
PR #28 is **merged** at `c3b4e83` (M4.1 UPI-First Checkout, Dynamic QR & Verified Payment Integration).
PR #29 is **merged** at `c71ec8d` (M4.2 Independent UPI Payment Security & Production Readiness Audit).
PR #30 is **open / under remediation** (M4.3 UPI Security Remediation, remediated by M4.3.1 on `feature/m4-3-1-review-remediation`).

## 4. Migrations per environment

`npm run vercel-build` runs `scripts/migrate.js` on every Vercel build. `migrationPolicy()` (`lib/db.js:87`) only allows Preview builds to migrate when `MIGRATE_ON_PREVIEW=true` **and** the build is isolated (`ISOLATED_PREVIEW_DB=true` or `PREVIEW_DATABASE_URL` set). `migrate()` records applied files **by filename only** in `schema_migrations`, with no checksum, so an edited migration file is never re-applied.

| Environment | DB endpoint | Applied | Evidence |
|---|---|---|---|
| **Local / CI** | ephemeral Postgres | whatever the checked-out tree has (`003`…`016` on `feature/m4-3-upi-security-remediation`) | CI service container; fresh each run. |
| **Local / CI** | ephemeral Postgres | whatever the checked-out tree has (`003`…`017` on `feature/m4-3-1-review-remediation`) | CI service container; fresh each run. |
| **Preview** (isolated) | `ep-green-hall-b47clnq0` (from earlier docs; the connection string was not read) | **`003` … `014`** (applied) | Applied via PR #23 push @ `d7836ff` (`dpl_AbjNDha7i298qjrh84FZZA3hHUQv`): `migration applied {"file":"014_location_remediation.sql"}`. |
| **Production** | `ep-dawn-glitter-b4mkwxkl` (from earlier docs; not re-verified) | **`003` … `009`** per its last build | Build log for `main` @ `5c6652a` (`dpl_3g7aUM3V…`, 2026-10-29): `[migrate] done (0 applied; vercel production)`; that tree holds `003`–`009`. |

Expand Down
11 changes: 7 additions & 4 deletions docs/UPI_PROVIDER_READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,15 @@ flowchart TD

### Readiness Status:

#### A. Code & Architectural Prerequisites (Engineering — Completed in M4.3)
#### A. Code & Architectural Prerequisites (Engineering — Completed in M4.3 & M4.3.1)
- [x] **Fix Webhook Signature Calculation**: Updated `verifyWebhookSignature` in `lib/upi/adapter.js` to compute `SHA256(response + saltKey) + "###" + saltIndex` on the decoded payload object. Verified with PhonePe S2S test.
- [x] **Strict Webhook Amount Verification**: Implemented in `applySuccessfulUpiPayment` and `processUpiWebhook`; underpayment is marked `disputed` with 0 bookings created.
- [x] **Strict Webhook Amount Verification (REV-04)**: Implemented in `applySuccessfulUpiPayment` and `processUpiWebhook`; requires safe positive integer exact paise match; currency and merchantId match; underpayment or malformed amounts are marked `disputed` with 0 bookings created.
- [x] **Merchant ID Webhook Verification (REV-06)**: Strictly verifies webhook `merchantId === this.merchantId`. Missing, null, or mismatched IDs return HTTP 401 and are rejected before processing.
- [x] **Enforce Authentication on `/checkout/:slug/upi`**: Gated unauthenticated users with redirect to `/signin`; guest checkout assigns guestToken and requires authenticated claim via `/api/checkout/upi/claim`.
- [x] **Active Intent Inventory Reservation**: Added `activePaymentHolds` and `activeTierPaymentHolds` to hold seats during unexpired intents; re-checks capacity at confirmation time.
- [x] **Fix QR Generator Bit Placement**: Corrected `lib/upi/qr.js` to prevent Dark Module overwriting and configured ISO 18004 Table 9 multi-block group capacities for Version 8.
- [x] **Duplicate Booking Protection (REV-02)**: Prevented duplicate active bookings during intent creation (409 `already_booked`), confirmation locked transaction (queued for refund in `refund_pending`), guest claims (409), and database constraint races (SQLSTATE `23505` savepoint recovery).
- [x] **Refund Operational Recovery & Reconciliation (REV-03)**: Permitted refunding payments in both `succeeded` and `refund_pending` states; isolated external gateway calls outside holding transactions; caught provider failures without losing audit state; added authorized manual finance reconciliation mechanism.
- [x] **Active Intent Inventory Reservation & Tier Index (REV-07)**: Added `activePaymentHolds` and `activeTierPaymentHolds` to hold seats during unexpired intents; added migration 017 index `idx_payments_active_tier_holds`; re-checks capacity at confirmation time.
- [x] **ISO/IEC 18004 Standard QR Compliance (REV-01, REV-05)**: Full support for QR Versions 1 through 14 (up to 362 bytes) with BCH(18, 6) Golay Version Information blocks, corrected alignment pattern placement, Dark Module preservation, user-facing 400 validation for overlong payloads, and 100% independent `jsQR` decoder validation.
- [x] **CSS Viewport Isolation**: Added media queries in `renderUpiCheckout` toggling display between `.checkout-mobile` and `.checkout-desktop`.
- [x] **Audit Ledger Immutability**: Added migration 016 with database trigger blocking UPDATE and DELETE on `payment_audits`.

Expand Down
26 changes: 26 additions & 0 deletions docs/UPI_SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,29 @@ All 9 audit findings were systematically resolved, implemented, and verified in
- **Prohibited Provider Scan**: 0 occurrences across entire repository (`test/superprofile-checkout.test.js`).
- **Linter**: `140/140 files OK` (`npm run lint`).
- **Safety Boundary**: `ENABLE_LIVE_PAYMENTS=false` strictly preserved across all environments.

---

## 6. Milestone M4.3.1 Independent Review Remediation Matrix (REV-01 through REV-07)

Following the submission of PR #30 (`feature/m4-3-upi-security-remediation`), an independent owner review identified 7 specific defects and merge blockers (REV-01 through REV-07). All 7 findings have been remediated, verified, and tested on branch `feature/m4-3-1-review-remediation`.

### Remediation Matrix

| Finding ID | Severity & Nature | Specific Defect | Remediation Implemented | Affected Files | Test Evidence |
|:---|:---|:---|:---|:---|:---|
| **REV-01** | **MERGE BLOCKER**<br>(Specification Conformance) | QR alignment patterns skipped on row/col 6 due to timing pattern reservations; Reed-Solomon polynomial math inverted; missing BCH(18, 6) Version Information for V7+ | Placed alignment patterns before timing patterns; corrected RS generator polynomial multiplication (`next[j] ^= g[j]; next[j+1] ^= gfMul(g[j], EXP[i])`); fixed ISO 18004 Table 12 Format Information bit ordering and dark module preservation; added BCH(18, 6) Golay Version Information blocks for V7–V14; verified independent decoding via `jsQR` | [`lib/upi/qr.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/upi/qr.js) | `test/upi-checkout.test.js` (`REV-01` test: 100% independent `jsQR` decode across V1–V14 and 243-byte UPI URI) |
| **REV-02** | **MERGE BLOCKER**<br>(Data Integrity & Race Conditions) | Duplicate active bookings allowed during payment intent creation and confirmation race; unhandled SQLSTATE `23505` threw unhandled HTTP 500 error | 1. Added pre-intent check in `createUpiPaymentIntent` rejecting active bookings with 409 `already_booked`.<br>2. Re-checked active bookings inside locked transaction in `applySuccessfulUpiPayment`, transitioning payment to `refund_pending` with reason `duplicate_booking` (0 bookings created).<br>3. Wrapped confirmation booking creation in `SAVEPOINT sp_confirm_booking` to catch SQLSTATE `23505` without HTTP 500.<br>4. Applied identical 409 protection and savepoint recovery to `claimUpiBooking` | [`lib/services/upi-payments.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/upi-payments.js) | `test/upi-checkout.test.js` (`REV-02` tests: intent creation 409, confirmation race `refund_pending`, guest claim 409, and SQLSTATE 23505 catch) |
| **REV-03** | **MERGE BLOCKER**<br>(Financial Operations) | Payments stuck in `refund_pending` had no operational recovery path; payments could not be refunded unless `succeeded`; provider refund failures rolled back audit state | 1. Permitted refund processing for both `succeeded` and `refund_pending` payments in `refundUpiOrder`.<br>2. Preserved audit ledger and idempotency (`status: 'refunded', duplicate: true`).<br>3. Moved external gateway network call outside DB holding transaction to avoid locking rows during gateway network I/O.<br>4. Caught provider network failures and persisted `refund_pending` with `failure_reason = 'refund_failed: ...'`.<br>5. Added authorized manual finance reconciliation mechanism (`manualReconciliation: true`) allowing finance officers to resolve stuck or failed refunds | [`lib/services/upi-payments.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/upi-payments.js), [`lib/services/refunds.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/refunds.js) | `test/upi-checkout.test.js` (`REV-03` tests: operational recovery from `refund_pending`, retry idempotency, provider 500 failure handling, and manual finance reconciliation) |
| **REV-04** | **MERGE BLOCKER**<br>(Financial Security) | `applySuccessfulUpiPayment` could receive missing, non-numeric, or malformed amounts; underpayment or currency mismatch was not strictly defended | 1. Enforced strict validation: `verifiedAmountPaise` must be a safe positive integer (`Number.isSafeInteger(n) && n > 0`).<br>2. Strictly verified `verifiedAmountPaise === Number(payment.amount)`.<br>3. Enforced exact currency match (`INR`) and merchant ID match.<br>4. Transitioned payment to `disputed` on any failure, logged audit details, and issued 0 bookings or Telegram invites | [`lib/services/upi-payments.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/upi-payments.js) | `test/upi-checkout.test.js` (`REV-04` tests: rejected null, NaN, non-numeric string, negative amount, underpayment, currency mismatch; 0 bookings created) |
| **REV-05** | **Medium**<br>(User Experience & Conformance) | Overlong UPI URI payloads crashed generator with unhandled HTTP 500; `buildUpiUri` could truncate payment details | 1. Expanded QR version capacity up to Version 14 (362 bytes), comfortably fitting large UPI URIs with long merchant notes and order IDs.<br>2. Enforced byte-length check using `Buffer.byteLength(uri, 'utf8')` rather than character count.<br>3. Created `QrPayloadTooLargeError extends HttpError` returning structured HTTP 400 with user-facing validation message.<br>4. Ensured `buildUpiUri` never truncates payment details | [`lib/upi/qr.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/upi/qr.js), [`lib/upi/adapter.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/upi/adapter.js) | `test/upi-checkout.test.js` (`REV-05` test: 363-byte payload rejected with HTTP 400 `payload_too_large` error rather than 500) |
| **REV-06** | **Medium**<br>(Webhook Authentication) | PhonePe webhook could bypass merchant ID checks if `merchantId` was absent or mismatched configured merchant identity | 1. In `PhonePeAdapter.verifyWebhook`: strictly verified `data.merchantId === this.merchantId`.<br>2. Rejected missing, null, or mismatched merchant ID with HTTP 401 `PaymentAdapterError`.<br>3. In `MockUpiAdapter`: verified `merchantId` against configured adapter identity.<br>4. Propagated `merchantId` into `applySuccessfulUpiPayment` verification | [`lib/upi/adapter.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/upi/adapter.js), [`lib/services/upi-payments.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/upi-payments.js) | `test/upi-checkout.test.js` (`REV-06` tests: missing merchantId rejected with 401; mismatched merchantId rejected with 401; valid merchantId accepted) |
| **REV-07** | **Low**<br>(Database Performance) | Ticket-tier active hold query `activeTierPaymentHolds` lacked an exact matching composite index | 1. Created additive migration `017_payments_tier_holds_index.sql` adding partial index `idx_payments_active_tier_holds` on `payments (((raw_ref->>'ticketTypeId')::uuid), status, expires_at) WHERE status = 'pending'`.<br>2. Verified index matches query predicate exactly.<br>3. Verified query plan uses Bitmap Index Scan via `EXPLAIN` | [`migrations/017_payments_tier_holds_index.sql`](file:///Users/pradeepjakuraa/strangersmeetup/migrations/017_payments_tier_holds_index.sql) | `test/migrations.test.js` (`REV-07` test: verifies index existence and EXPLAIN execution plan using `idx_payments_active_tier_holds`) |

### M4.3.1 Verification Summary
- **Total Test Suite**: 466 tests total, 464 passed, 2 skipped (unconfigured Stars sandbox), 0 failed (`duration: ~50s`).
- **UPI Checkout Suite**: 31/31 passed (`test/upi-checkout.test.js`).
- **Migration Suite**: 8/8 passed (`test/migrations.test.js`, migrations `003` through `017` verified).
- **Linter**: `140/140 files OK` (`npm run lint`).
- **Playwright E2E**: Verified against `sm_e2e`.
- **Absolute Safety Boundary**: `ENABLE_LIVE_PAYMENTS=false` strictly preserved across all files and environments. Real money transactions remain 100% disabled.
8 changes: 8 additions & 0 deletions lib/services/refunds.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import { tg } from '../telegram/api.js';
import { enqueue, deliverMany } from './notifications.js';
import { audit } from '../audit.js';
import { removeFromChat, tierChatId } from './invites.js';
import { refundUpiOrder } from './upi-payments.js';

export async function refundPayment(paymentId, actor) {
const p = await db.one(
`SELECT p.*, u.telegram_user_id FROM payments p LEFT JOIN users u ON u.id = p.user_id WHERE p.id = $1`, [paymentId]);
if (!p) throw new HttpError(404, 'not_found', 'Payment not found');

// REV-03: Authorized admin/finance refund for UPI direct payments
if (p.provider === 'phonepe' || p.provider === 'upi_mock') {
const res = await refundUpiOrder(p.order_id, { actor });
return { ok: true, ...res };
}

if (p.status !== 'paid') throw new HttpError(409, 'not_refundable', `Payment is ${p.status}`);

// Claim the refund atomically so two concurrent clicks cannot refund twice.
Expand Down
Loading
Loading