From 3362e2e1b8a2f5dfeea0d82d7c23de4e5734cbac Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sun, 7 Jun 2026 08:01:15 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20sync=20from=20backend=20c3ad979=20?= =?UTF-8?q?=E2=80=94=20medium=20transaction=20audit=20closeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 09 - Audits/Activity Log.md | 10 ++++++++++ .../DB Query & Schema Audit - 2026-06-06.md | 15 +++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/09 - Audits/Activity Log.md b/09 - Audits/Activity Log.md index 78c48bb..db41f59 100644 --- a/09 - Audits/Activity Log.md +++ b/09 - Audits/Activity Log.md @@ -12,6 +12,16 @@ entries on top. Maintained by agents per the rule in `../AGENTS.md`. --- +### 2026-06-07 — backend@c3ad979, frontend@a8791b1 — DB audit medium transaction closeout M13/M14/M17 + +**Commits:** `c3ad979` `a8791b1` +**Touched:** backend `src/services/marketplace/RequestTemplateService.ts`, `src/db/repositories/drizzle/DrizzleMarketplaceRepo.ts`, `src/db/repositories/interfaces/IMarketplaceRepo.ts`, `src/services/payment/paymentCoordinator.ts`, `src/services/auth/authStore.ts`, `src/services/user/userController.ts`, `__tests__/request-template-batch-convert-cache.test.ts`, `__tests__/payment-coordinator.test.ts`, `__tests__/auth-store-pg-query.test.ts`, `__tests__/db-audit-money-flow-transactions.test.ts`, `package.json`, `package-lock.json`; frontend `Dockerfile`, `package.json`; docs `09 - Audits/DB Query & Schema Audit - 2026-06-06.md`, `09 - Audits/Activity Log.md` +**Why:** Close Medium M13/M14/M17 from the DB Query & Schema Audit. Single template conversion now creates the request and initial offer through one serializable vital-db repo transaction while leaving usage accounting on the intentionally non-vital side. PG payment completion now has a real notify-only follow-up path that skips DB writes. Profile email verification now promotes `pending_email` through one conditional SQL update with explicit conflict handling. +**Verification:** backend `npm run typecheck`, `npm test -- --runTestsByPath __tests__/request-template-batch-convert-cache.test.ts __tests__/payment-coordinator.test.ts __tests__/auth-store-pg-query.test.ts __tests__/db-audit-money-flow-transactions.test.ts --runInBand` (4 suites / 23 tests), `scripts/smoke/db-audit-service-regressions.sh` (18 suites / 69 tests); frontend version check confirmed no tracked `2.9.33` references outside `.git`. Pushed to Forgejo. +**Linked docs updated:** [[09 - Audits/DB Query & Schema Audit - 2026-06-06]] + +--- + ### 2026-06-07 — backend@5364704, frontend@c34ab0a — DB audit money-flow transaction closeout H14/H15/H27/H29 **Commits:** `5364704` `c34ab0a` diff --git a/09 - Audits/DB Query & Schema Audit - 2026-06-06.md b/09 - Audits/DB Query & Schema Audit - 2026-06-06.md index e30b844..495e108 100644 --- a/09 - Audits/DB Query & Schema Audit - 2026-06-06.md +++ b/09 - Audits/DB Query & Schema Audit - 2026-06-06.md @@ -81,6 +81,9 @@ updated: 2026-06-07 | H15: legacy Request Network confirmation webhook split payment/PR writes → confirmation routed through `PaymentCoordinator` | `5364704` v2.9.33 | | H27: manual `verifyPayment` created completed row before propagation → create pending, then complete through `PaymentCoordinator` | `5364704` v2.9.33 | | H29: `updatePurchaseRequest.selectedOfferId` unguarded update → serializable PR row lock plus selected-offer ownership lock | `5364704` v2.9.33 | +| M13: single template conversion request+offer writes split across service calls → dedicated serializable repo transaction for vital request/offer writes | `c3ad979` v2.9.34 | +| M14: PG payment completion follow-up assumed DB idempotency → explicit `notifyOnly` path skips all DB writes | `c3ad979` v2.9.34 | +| M17: profile email verification pending-email race → single conditional SQL `UPDATE` with conflict outcome handling | `c3ad979` v2.9.34 | --- @@ -724,21 +727,21 @@ The PgQuery wrapper's `.sort()`, `.skip()`, `.limit()` methods are applied insid ### 13. convertTemplateToRequest creates a request then offer with no transaction -> **Category:** Missing Transaction | **File:** `src/services/marketplace/RequestTemplateService.ts:362-395` +> **Category:** Missing Transaction | **File:** `src/services/marketplace/RequestTemplateService.ts:362-395` | **FIXED** `c3ad979` v2.9.34 `convertTemplateToRequest` calls `createPurchaseRequest` (line 362), conditionally `createTemplateOffer` (line 390), `incrementUsageCount` (line 393), and `findPurchaseRequestById` (line 394) as separate operations. A failure between any two steps leaves the database in a partially created state. -**Fix:** Wrap all writes for a single template conversion in a database transaction. +**Fix:** Single conversion now builds the request and optional template offer payloads, then calls `createTemplatePurchaseRequest`, which inserts the `purchase_requests` row, child rows, and initial `seller_offers` row inside one serializable vital-db transaction. Template usage accounting remains a non-vital post-commit side effect because SEC-007 intentionally splits `request_templates` onto the non-vital DB role; the partial money-side request/offer state is closed. --- ### 14. executePaymentUpdate re-runs updatePurchaseRequestStatus after the PG transaction commits — fragile idempotency assumption -> **Category:** Missing Transaction | **File:** `src/services/payment/paymentCoordinator.ts:496-537` +> **Category:** Missing Transaction | **File:** `src/services/payment/paymentCoordinator.ts:496-537` | **FIXED** `c3ad979` v2.9.34 After the PG transaction commits (line 365), the code unconditionally calls `updatePurchaseRequestStatus` again at line 500 ('Re-run in notification-only mode'). That method calls `new SellerOfferService().acceptOffer(selectedOfferId)`, which may issue additional DB writes even though the transaction already flipped the offer statuses. Idempotency is assumed but not enforced by any DB constraint. -**Fix:** Pass a `notifyOnly: true` flag to `updatePurchaseRequestStatus` that skips all DB writes entirely when `pgTransactionCompleted` is true, and only emits socket events and notifications. +**Fix:** PG completion now passes `{ notifyOnly: true }` into `updatePurchaseRequestStatus`. The method treats the already-loaded request as the event source in notify-only mode and skips both `updatePurchaseRequest` and `SellerOfferService.acceptOffer`, emitting only socket events/notifications after the committed transaction. --- @@ -764,11 +767,11 @@ The function reads the deleted user (line 721) then updates it (line 725) in two ### 17. verifyCurrentUserEmail: race between pendingEmail uniqueness check and promotion -> **Category:** Missing Transaction | **File:** `src/services/user/userController.ts:538-557` +> **Category:** Missing Transaction | **File:** `src/services/user/userController.ts:538-557` | **FIXED** `c3ad979` v2.9.34 The controller checks `User.findOne({ email: user.pendingEmail, _id: { $ne: userId } })` (line 539) then promotes `user.email = user.pendingEmail` (line 546) as two independent operations. A concurrent registration could claim the same email address between the check and the save, bypassing the uniqueness guard. -**Fix:** Use a single `UPDATE users SET email = pendingEmail WHERE id = $1 AND NOT EXISTS (SELECT 1 FROM users WHERE email = pendingEmail AND id <> $1)` in one statement, or wrap with `SELECT … FOR UPDATE`. +**Fix:** `verifyCurrentUserEmail` now delegates to `verifyAndPromoteProfileEmail`, a single CTE-backed SQL statement that validates the code, checks pending-email ownership, promotes `pending_email` to `email`, clears verification fields, and returns an explicit `verified` / `invalid` / `email_exists` outcome. Concurrent unique-email violations are mapped back to the existing `EMAIL_EXISTS` response path. ---