docs: sync from backend c3ad979 — medium transaction audit closeout
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user