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
8 changes: 5 additions & 3 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,19 @@ Local `main` in the main clone is at `5693f7b`, an older Production commit that
|---|---|---|---|---|
| #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 |
| #31 M4.3.1 Review Remediation | `feature/m4-3-upi-security-remediation` ← `feature/m4-3-1-review-remediation` | `03bf518` | `lint, unit, e2e` ✅ | Independent Review Corrections for PR #30 |
| (Remediation) M4.3.2 Financial Integrity Remediation | `feature/m4-3-1-review-remediation` ← `feature/m4-3-2-financial-integrity` | (work branch) | `lint, unit, e2e` ✅ | Fail-Closed Merchant Identity, Secure Manual Reconciliation & State Transition Verification |

**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.
**Owner gate:** PR #17 must not be merged without the owner's separate approval. `main` has no GitHub Actions runs on record. PR #30, PR #31, and PR #17 must not be merged automatically without owner review. Keep `ENABLE_LIVE_PAYMENTS=false` everywhere.

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`).
PR #30 is **open / preserved** (`feature/m4-3-upi-security-remediation`).
PR #31 is **open / preserved** (`feature/m4-3-1-review-remediation`).

## 4. Migrations per environment

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

### Readiness Status:

#### A. Code & Architectural Prerequisites (Engineering — Completed in M4.3 & M4.3.1)
#### A. Code & Architectural Prerequisites (Engineering — Completed in M4.3, M4.3.1 & M4.3.2)
- [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 (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] **Fail-Closed Merchant Identity & Currency Invariance (M4.3.2)**: Replaced permissive merchant logic with strict fail-closed verification across verified, stored, and server-configured merchant IDs. Rejects missing, null, empty string, or mismatched merchant IDs across PhonePe PG and mock adapters. Enforces non-empty `'INR'` currency matching.
- [x] **Secure Manual Refund Reconciliation (M4.3.2)**: Prohibited generated or placeholder `MANUAL-...` references. Implemented `verifyFinanceAuthorization` checking active database roles (`super_admin`, `finance`), `isValidExternalRefundRef` requiring verifiable external references (bank UTR, PG refund ARN), and `isValidEvidenceRef` requiring documented evidence references recorded in the immutable audit ledger. Documented operational CBS limitation.
- [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] **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] **Refund Operational Recovery & Reconciliation (REV-03, M4.3.2)**: 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; ensured booking revocation and chat removals fire only upon confirmed refund.
- [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`.
Expand Down
29 changes: 29 additions & 0 deletions docs/UPI_SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,32 @@ Following the submission of PR #30 (`feature/m4-3-upi-security-remediation`), an
- **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.

---

## 7. Milestone M4.3.2 Financial Integrity & Independent Review Corrections

Following independent owner review of PR #31 (`feature/m4-3-1-review-remediation`), additional financial-integrity vulnerabilities were identified in merchant identity validation, manual refund reconciliation, and refund state machine lifecycle enforcement. All findings have been fully remediated and verified in Milestone M4.3.2 on branch `feature/m4-3-2-financial-integrity`.

### Remediation Matrix

| Finding ID | Severity & Nature | Specific Defect | Remediation Implemented | Affected Files | Test Evidence |
|:---|:---|:---|:---|:---|:---|
| **SEC-REV-01** | **CRITICAL**<br>(Financial Integrity) | Permissive merchant check `!merchantId \|\| !payment.merchant_id \|\| merchantId === payment.merchant_id` allowed missing or unconfigured merchant IDs to confirm payments; currency validation could be bypassed with missing/empty currency | 1. Replaced permissive logic with strict fail-closed validation.<br>2. Verified merchant ID from gateway must be non-empty and present.<br>3. Stored expected merchant ID in payment row must be non-empty and present.<br>4. Configured server-side merchant ID on adapter must be non-empty and present.<br>5. Enforced constant-time equality across all three identities (`safeEqual`).<br>6. In PhonePe and Mock adapters, strictly rejected missing, null, empty string, or mismatched merchant IDs on webhooks.<br>7. In `createUpiPaymentIntent`, rejected intent creation if adapter merchant ID is not configured.<br>8. Enforced currency validation: incoming currency must be non-empty, equal `'INR'`, and match payment currency `'INR'`. Transitioned payment to `disputed` on any failure with 0 bookings created | [`lib/services/upi-payments.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/upi-payments.js), [`lib/upi/adapter.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/upi/adapter.js) | `test/upi-checkout.test.js` (`M4.3.2: Merchant identity must fail closed across adapters and payment confirmation` test: missing, null, empty, and mismatched IDs rejected; missing and mismatched currencies rejected) |
| **SEC-REV-02** | **HIGH**<br>(Fraud Prevention & Audit Integrity) | Manual refund reconciliation generated placeholder `MANUAL-...` references without proof of external transaction; trusted caller-provided actor strings without server-side verification; lacked mandatory evidence references | 1. Implemented `verifyFinanceAuthorization`: verified actor identity and active `super_admin` or `finance` role against real PostgreSQL `admin_users` records.<br>2. Implemented `isValidExternalRefundRef`: required mandatory external refund transaction reference (e.g. bank UTR, PG refund ARN, IMPS/NEFT ref); rejected missing, empty, or placeholder references (starting with or equal to "MANUAL", "DUMMY", "TEST", "PLACEHOLDER", repeated characters, sequence patterns).<br>3. Implemented `isValidEvidenceRef`: required documented reconciliation evidence reference (e.g. support ticket ID, bank statement entry reference).<br>4. Implemented `isValidReconciliationReason`: required documented reason (minimum 10 chars).<br>5. Enforced row-level locking (`SELECT ... FOR UPDATE`) in `refundUpiOrder` to guarantee strict idempotency between concurrent operators.<br>6. Recorded authenticated actor, timestamp, reference, and evidence in immutable `payment_audits` and `audit_log`.<br>7. Gated admin HTTP refund endpoint `POST /api/admin/payments/:id/refund` with strict body schema validation and role authorization | [`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), [`lib/handlers/admin.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/handlers/admin.js) | `test/upi-checkout.test.js` (`M4.3.2: Secure manual refund reconciliation authorization, evidence reference and state transitions` test: unauthorized role 403, placeholder ref 400, missing evidence 400, authorized reconciliation 200, idempotency verified) |
| **SEC-REV-03** | **MEDIUM**<br>(State Machine Conformance) | Provider refund initiation could prematurely trigger refund side effects (invite revocation, booking cancellation) before gateway or reconciliation confirmation | 1. Confirmed both `succeeded` and `refund_pending` can enter the refund workflow; all other statuses rejected with 409 `not_refundable`.<br>2. Strictly distinguished provider initiation from completion: provider pending response (`PAYMENT_PENDING`) updates payment to `refund_pending` and does NOT mark refunded.<br>3. Failed provider refunds remain recoverable in `refund_pending` with documented failure reason.<br>4. Booking status updates to `refunded`, invite link revocations, chat member removals, and notification delivery execute **only** when `status === 'refunded'` is confirmed by provider or authorized reconciliation | [`lib/services/upi-payments.js`](file:///Users/pradeepjakuraa/strangersmeetup/lib/services/upi-payments.js) | `test/upi-checkout.test.js` (`M4.3.2` tests: pending provider response leaves payment in `refund_pending` with active booking; failed refund remains recoverable; side effects fire only on confirmed completion) |

### Evidence Storage Limitation Notice

> [!IMPORTANT]
> **Minimal Reference-Based Reconciliation Architecture Limitation**:
> NIGHTLIFEZ currently implements a safe, reference-based manual refund reconciliation workflow that links verifiable external banking/gateway reference numbers (e.g. bank UTR, payment gateway refund ARN) and internal audit evidence references (e.g. finance ticketing identifiers) directly into the immutable `payment_audits` ledger and the `payments.raw_ref->'reconciliation'` document.
>
> *Limitation*: This mechanism relies on verifiable reference strings and verified finance operator authorization; it does not interface directly with Core Banking System (CBS) APIs or host automated cryptographic banking settlement verification. Finance operators must follow operational standard operating procedures (SOPs) to ensure that the external refund reference corresponds to a verified entry on the corporate bank statement before initiating manual reconciliation.

### M4.3.2 Verification Summary
- **Total Test Suite**: 468 tests total, 466 passed, 2 skipped (unconfigured Stars sandbox), 0 failed (`duration: ~47s`).
- **UPI Checkout Suite**: 33/33 passed (`test/upi-checkout.test.js`).
- **Migration Suite**: 8/8 passed (`test/migrations.test.js`).
- **Linter**: `140/140 files OK` (`npm run lint`).
- **Absolute Safety Boundary**: `ENABLE_LIVE_PAYMENTS=false` preserved across all files and environments. Real money transactions remain 100% disabled. Merges to PR #31, PR #30, and PR #17 strictly prohibited.
24 changes: 22 additions & 2 deletions lib/handlers/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,30 @@ export async function linkOrder(ctx) {
sendJson(ctx.res, 200, { ok: true, bookingId: r.bookingId });
}

const RefundBody = z.object({
reason: z.string().max(500).optional(),
manualReconciliation: z.boolean().optional(),
externalRefundRef: z.string().max(120).optional(),
evidenceRef: z.string().max(255).optional()
}).strict().optional();

export async function refundPayment(ctx) {
const a = await guard(ctx, R.REFUND, { write: true });
await refundSvc(uuidParam(ctx.params.id), a.actor);
sendJson(ctx.res, 200, { ok: true });
let body = {};
try {
body = await readJson(ctx.req);
} catch {
body = {};
}
const parsed = parse(RefundBody, body || {}) || {};
const res = await refundSvc(uuidParam(ctx.params.id), a.actor, {
reason: parsed.reason,
manualReconciliation: parsed.manualReconciliation,
externalRefundRef: parsed.externalRefundRef,
evidenceRef: parsed.evidenceRef,
sessionAdmin: a.admin
});
sendJson(ctx.res, 200, { ok: true, ...res });
}

// ---------- memberships ----------
Expand Down
13 changes: 10 additions & 3 deletions lib/services/refunds.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ import { audit } from '../audit.js';
import { removeFromChat, tierChatId } from './invites.js';
import { refundUpiOrder } from './upi-payments.js';

export async function refundPayment(paymentId, actor) {
export async function refundPayment(paymentId, actor, options = {}) {
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
// Authorized admin/finance refund for UPI direct payments
if (p.provider === 'phonepe' || p.provider === 'upi_mock') {
const res = await refundUpiOrder(p.order_id, { actor });
const res = await refundUpiOrder(p.order_id, {
actor,
reason: options.reason || 'Customer requested refund',
manualReconciliation: Boolean(options.manualReconciliation),
externalRefundRef: options.externalRefundRef || options.providerRefundId,
evidenceRef: options.evidenceRef,
sessionAdmin: options.sessionAdmin
});
return { ok: true, ...res };
}

Expand Down
Loading
Loading