docs: sync from backend c3ad979 — medium transaction audit closeout

This commit is contained in:
Siavash Sameni
2026-06-07 08:01:15 +04:00
parent 4ba2a556f7
commit 3362e2e1b8
2 changed files with 19 additions and 6 deletions

View File

@@ -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 ### 2026-06-07 — backend@5364704, frontend@c34ab0a — DB audit money-flow transaction closeout H14/H15/H27/H29
**Commits:** `5364704` `c34ab0a` **Commits:** `5364704` `c34ab0a`

View File

@@ -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 | | 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 | | 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 | | 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 ### 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. `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 ### 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. 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 ### 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. 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.
--- ---