Merge branch 'main' of ssh://git.tbs.amn.gg:2222/escrow/nick-doc

# Conflicts:
#	09 - Audits/Activity Log.md
This commit is contained in:
Siavash Sameni
2026-06-05 07:51:18 +04:00

View File

@@ -11,6 +11,123 @@ entries on top. Maintained by agents per the rule in `../AGENTS.md`.
---
### 2026-06-05 — backend@v2.8.84, frontend@v2.8.101 — Notifications wired end-to-end (id-seam normalize + Mini App joins user room)
**Commits:** backend v2.8.84, frontend v2.8.101
**Touched:** backend `services/notification/NotificationService.ts`; frontend `sections/telegram/hooks/use-telegram-notifications.ts`
**Why:** No notifications reached the Mini App (in-app, real-time, or Telegram push). Two root causes. (1) **Id seam:** notifications were created with whatever id the event carried — usually a Postgres uuid (`sellerId`/`buyerId`) — but everything that *consumes* a notification keys on the user's session legacy ObjectId: the in-app fetch (`req.user.id`), the `user-<id>` socket room, and `TelegramLink.userId`. So a notification stored under a uuid was invisible to fetch, never reached the room, and `sendTelegramNotificationToUser(uuid)` found `no_link`. Fix: `NotificationService.createNotification`/`createNotificationsBulk` now normalise `userId` via `toCanonicalUserId` (uuid → `users.legacy_object_id`) before persist + real-time emit + Telegram forward, so all three line up. (2) **Mini App never joined its room:** the socket connected but `use-telegram-notifications` only registered listeners — it never emitted `join-user-room`, so the backend's targeted `new-notification` emits had no subscriber. Added a `joinUserRoom(userId)` effect that (re)joins on every connect. `selfId` is the session ObjectId, matching the socket's authed id and the now-normalised emit target.
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — clean. After deploy: a marketplace event (offer received/accepted, payment, delivery) creates a notification that (a) shows in the Mini App bell list, (b) bumps the unread badge live, and (c) arrives as a Telegram bot message for linked users.
---
### 2026-06-05 — backend@v2.8.83, frontend@v2.8.100 — Select-offer 403 (id seam) + offer delivery-time `[object Object]`
**Commits:** backend v2.8.83, frontend v2.8.100
**Touched:** backend `services/marketplace/marketplaceController.ts` (`selectOffer`); frontend `sections/telegram/view/telegram-request-detail-view.tsx`
**Why:** Testing the new Mini App «انتخاب و پرداخت» surfaced two bugs. (1) **403 on select-offer**: `selectOffer` gated buyer ownership with `toIdString(purchaseRequest.buyerId) !== buyerId` — the session legacy ObjectId vs the PG-uuid buyerId (the recurring seam) → the real buyer got 403. Switched to `!(await sameUser(buyerId, toIdString(purchaseRequest.buyerId)))`. (2) **Delivery time rendered `[object Object]`**: the offer card assumed `offer.deliveryTime` was a number, but it can be an object (`{amount, unit}`). Now extracts `dt.amount ?? dt.value ?? offer.deliveryTimeAmount` when it isn't a plain number.
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — clean. After deploy: buyer taps «انتخاب و پرداخت» → offer accepted (no 403) → in-shell payment opens; delivery time shows a number of days, not `[object Object]`.
---
### 2026-06-05 — frontend@v2.8.99 — Mini App buyer: see received offers + select & pay in-shell
**Commits:** frontend only (v2.8.99)
**Touched:** `sections/telegram/view/telegram-request-detail-view.tsx`, `sections/telegram/locales/{fa,en,types}.ts` (`offers_title`, `offer_select_pay`, `offer_delivery_days`, `offer_accepting`)
**Why:** When the buyer's stepper reached «انتخاب و پرداخت» (step 3, status `received_offers`) the Mini App had no way to act — the pay CTA only showed for `pending_payment`. Now, for a buyer on an offer-selection status, the detail view fetches and lists the received offers (seller, price, delivery days, description) and each has a «انتخاب و پرداخت» button → `acceptOffer(requestId, offerId)` (sets `selectedOfferId`, moves to `pending_payment`) → `refresh()` → opens the in-shell direct-transfer payment screen via `onPay`. Reused the existing offers SWR (broadened from review-only to also fire on `received_offers`/`offers_received`/`in_negotiation`).
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: buyer opens a request with an offer → sees the offer card → «انتخاب و پرداخت» → in-shell payment opens; paying advances the stepper.
---
### 2026-06-05 — frontend@v2.8.98 — Mini App seller shop: product-type filter + show controls for small shops
**Commits:** frontend only (v2.8.98)
**Touched:** `sections/telegram/view/telegram-seller-shop-view.tsx`
**Why:** The seller shop (per-seller template list) already had search + sort but (1) no filter, and (2) the controls bar was gated on `allTemplates.length > 3`, so a typical 3-template shop showed no controls at all. Added a product-type filter (chips built only for the types the seller actually sells — physical/digital/service/consultation, plus «همه») wired through `TelegramListControls`, and lowered the controls threshold to `> 1` so shops with 23 templates still get search/filter/sort.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: opening a seller shop with ≥2 templates shows the controls bar; the filter chips reflect the seller's product types and narrow the list.
---
### 2026-06-04 — backend@v2.8.82, frontend@v2.8.97 — Stepper advances: offer→step, seller offer detection, real-time on code-verify
**Commits:** backend v2.8.82, frontend v2.8.97
**Touched:** backend `services/marketplace/marketplaceController.ts` (`getOffersForRequest` sellerId filter, `verifyDeliveryCode` socket emit); frontend `sections/request/request-config.tsx` (buyer `received_offers`→3), `actions/marketplace.ts` (`getSellerOfferForRequest` fallback)
**Why:** Three related stepper bugs surfaced while testing an offer end-to-end. (1) **Buyer stepper stuck at "awaiting offers" (2)** after an offer arrived: `determineBuyerStep('received_offers')` returned `offersCount>0 ? 3 : 2`, but callers that don't thread `offersCount` (the Mini App) got 2. The status itself means an offer arrived on the buyer's own request, so it now returns 3 unconditionally. (2) **Seller stuck on "send offer" (1)** after submitting: `getSellerOfferForRequest` matched offers by `o.sellerId === user._id`, but `o.sellerId` is a PG uuid and `user._id` is the legacy ObjectId (the seam) → no match → `sellerOfferStatus` undefined → step 1. Backend `getOffersForRequest` now filters to the seller's own offers via `sameUser` when `?sellerId=` is passed, and the client returns `data[0]` as a fallback. (3) **Seller needed a manual refresh** to go from "awaiting buyer" (4) to "receive funds" (5): the seller's code entry (`verifyDeliveryCode`) replaced the buyer's `confirmDelivery` in v2.8.95 but never emitted a socket event, so no live update. Added the `purchase-request-update` / `status-changed` emit on successful verify (matching what `confirmDelivery` did).
**Verification:** backend `npx tsc --noEmit`; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint — all clean. After deploy: buyer sees step 3 once an offer arrives; seller sees step 2 right after sending an offer; seller's stepper advances to "receive funds" without a manual refresh after entering the code.
---
### 2026-06-04 — frontend@v2.8.96 — Mini App account: don't show "email verified" when the user has no email
**Commits:** frontend only (v2.8.96)
**Touched:** `sections/telegram/view/telegram-account-view.tsx`, `sections/telegram/locales/{fa,en,types}.ts` (`email_not_set`)
**Why:** A Telegram sign-up has `isEmailVerified=true` but no email address, so the account header showed a green «ایمیل تأیید شده» badge even though the email field was empty. Gated the verified badge on `user?.email && user?.isEmailVerified`. Added a third state for the no-email case: a «ایمیل ثبت نشده» badge that opens the in-shell settings (`onOpenSettings`) to add an email. The not-verified (has-email-but-unverified) branch is unchanged.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: a Telegram user with no email sees «ایمیل ثبت نشده» (→ settings), not a false «ایمیل تأیید شده».
---
### 2026-06-04 — frontend@v2.8.95 — Delivery confirmation: code is entered by the seller, buyer's «تایید دریافت کالا» removed
**Commits:** frontend only (v2.8.95)
**Touched:** `sections/telegram/view/telegram-request-detail-view.tsx` (Mini App buyer: drop confirm button), `sections/request/components/buyer-steps/step-5-receive-goods.tsx` (web buyer: drop confirm button), `sections/request/components/seller-steps/step-4-waiting-for-confirmation.tsx` (web seller: code display → code input)
**Why:** The handover flow was wrong/duplicated: the buyer had a «تایید دریافت کالا» button (`confirmDelivery`) AND the seller was shown the expected code. Per the user, the single correct mechanism is: the **buyer gives the 6-digit code, the seller enters it** to confirm — and that 403'd anyway (`confirmDelivery` compared session ObjectId `!==` PG-uuid buyerId — same id seam). Changes: (1) Mini App buyer (`status delivery`) now only displays the code to hand over — the `confirmDelivery` button/handler/import removed. (2) Web buyer step-5: removed the «تایید دریافت کالا» button (kept the code display + an explanatory note). (3) Web seller step-4: replaced the code **display** (`getDeliveryCode`, which leaked the code to the seller and 403s post-v2.8.79) with a 6-digit **input** + «تایید تحویل کالا» calling `verifyDeliveryCode(id, code)`. Both `confirmDelivery` and `verifyDeliveryCode` reach the same `delivered` state, so no escrow-release regression. (Mini App seller already had the input from v2.8.94.)
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. After deploy: buyer sees only the code (no confirm button); seller enters the buyer's code → status → delivered; no 403.
---
### 2026-06-04 — backend@v2.8.81 — Public shop settings: resolve uuid→legacy ObjectId (name/avatar were blank publicly)
**Commits:** backend v2.8.81
**Touched:** `services/marketplace/shopSettingsStore.ts` (`getSellerShopSettings` + new `resolveSellerLegacyId`)
**Why:** Found while setting shop name/avatar/cover for the three seed sellers. After PUT-ing settings, the seller's OWN `GET /marketplace/shop/settings` returned them correctly, but the PUBLIC `GET /marketplace/shop/settings/:sellerId` (what the shop page shows buyers) returned `data:null` — blank name, no avatar/cover. Root cause: the recurring Mongo↔PG id seam. `shop_settings` rows are keyed by `seller_legacy_object_id` (the seller's legacy Mongo ObjectId, which is what `req.user.id` is on the own-settings path), but the public sellers list returns each seller by their PG **uuid**, so `getSellerShopSettings(uuid)` queried `where seller_legacy_object_id = <uuid>` and matched nothing. Fix: when the direct lookup misses, resolve the incoming id via `select legacy_object_id from users where id=$1` (a 24-hex id is treated as already-legacy) and retry. Mirrors the existing reverse `resolveSellerUuid` helper.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: `GET /marketplace/shop/settings/<pg-uuid>` returns the saved name/avatar/cover for all three sellers (was null). Seeded: seller/seller1/seller2 @amn.gg each have a shop name, description, avatar, cover image, social links, and 3 active products.
---
### 2026-06-04 — backend@v2.8.80 — Shop sellers list cache: invalidate global `templates:list` on per-seller change
**Commits:** backend v2.8.80
**Touched:** `services/redis/cacheService.ts` (`invalidateTemplatesCache`)
**Why:** Found while seeding shops for three sellers via API: after creating templates for `seller1@amn.gg`/`seller2@amn.gg`, the public shop endpoint (`GET /marketplace/request-templates/sellers`) still returned only the first seller. Root cause — the global sellers list is cached under Redis key `templates:list`, but `RequestTemplateService` create/update/delete call `invalidateTemplatesCache(sellerId)` which only deleted `templates:seller:<id>`, never `templates:list`. So a newly-onboarded seller didn't appear in `/shop` until the 5-minute TTL elapsed. Fix: when invalidating with a `sellerId`, also `del('templates:list')` since the aggregated list changes whenever any seller adds/edits/removes a template.
**Verification:** backend `npx tsc --noEmit` clean. After deploy: a brand-new seller's first template appears in `GET …/request-templates/sellers` (and dev.amn.gg/shop) immediately, not after TTL. (Seeded data: seller/seller1/seller2 @amn.gg each have 3 active templates — two at 0.01 USDT, one at 0.05 — with images, BSC USDT/USDC payment config.)
---
### 2026-06-04 — frontend@v2.8.94 — Telegram Mini App: seller delivery-code entry field
**Commits:** frontend only (v2.8.94); backend stays v2.8.79
**Touched:** `telegram-request-detail-view.tsx` (seller code-entry card), `locales/{fa,en,types}.ts` (`seller_code_*` strings)
**Why:** The escrow handover is two-sided: the buyer generates a 6-digit delivery code (shown in v2.8.92) and the **seller enters it** to confirm delivery. The Mini App seller had no field for this — at status `delivery` the seller only saw «منتظر تایید خریدار» with nowhere to type the code. Added a code-entry card for `role === 'seller' && status === 'delivery'`: a numeric 6-digit input (digit-filtered, monospace, centred) + «تایید تحویل کالا» button calling `verifyDeliveryCode(requestId, code)` then `refresh()` so the status advances to `delivered`. Backend `verifyDeliveryCode` already uses the `sameUser` seller gate (v2.8.77), so this composes with the id-seam fix.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. Admin verifies after deploy: buyer opens `delivery` request → sees code; seller opens same request in Mini App → enters that code → «تایید تحویل کالا» → status → delivered.
---
### 2026-06-04 — backend@v2.8.79 — Delivery code: apply `sameUser` to buyer auth gates (Mini App showed "—")
**Commits:** backend v2.8.79 (frontend stays v2.8.93)
**Touched:** `services/marketplace/marketplaceController.ts` (`generateDeliveryCode`, `getDeliveryCode`)
**Why:** In the Mini App receive-goods step the delivery code rendered as «—»: both `getDeliveryCode` and `generateDeliveryCode` returned 403. Root cause is the recurring Mongo↔PG id seam — these two handlers still used the strict `request.buyerId !== userId` comparison (session legacy ObjectId vs PG-uuid `buyerId`), unlike `verifyDeliveryCode` which was already moved to the async `sameUser` helper in v2.8.77. Replaced both buyer checks with `!(await sameUser(userId, toIdString(request.buyerId)))`. Added an explicit `!userId` unauthorized guard in `getDeliveryCode` to restore the `string` narrowing the removed `!==` previously provided (so `getDeliveryCode(id, userId)` still type-checks). The repo layer (`getDeliveryCodeForUser`) already resolves the seam via `resolveEntityId`, so once the code exists it loads without regenerating.
**Verification:** backend `npx tsc --noEmit` clean. Admin verifies after deploy: buyer opens a `delivery`-status request in the Mini App → a 6-digit delivery code appears (no «—», no 403); seller can verify it.
---
### 2026-06-04 — backend@v2.8.78, frontend@v2.8.93 — Gate shop orders on payment: abandon-cancel + 30-min TTL sweep
**Commits:** backend v2.8.78, frontend v2.8.93
**Touched:** backend `services/admin/dataCleanupService.ts` (new `cancelStaleUnpaidRequests`), `services/admin/ttlCleanupJob.ts` (schedule it every 5 min); frontend `telegram-payment-view.tsx` (abandon-cancel on unmount)
**Why:** Shop checkout creates the purchase request in `pending_payment` *before* the buyer pays — unavoidable because the direct-balance deposit address is derived per-request, so the request must exist for the AMN scanner to credit it (the payment-intent route requires `purchaseRequestId`/`sellerOfferId`/`sellerId`). That left unpaid orders lingering. Now, per the user's choice (both mechanisms, 30-min window, only cancel orders with **no** deposit): (1) **Frontend abandon-cancel** — when the buyer leaves the in-shell checkout payment screen without completing, the just-created order is set to `cancelled` via `updateRequestStatus`; two refs (`paidRef`, `depositRef`) suppress the cancel if the deposit was confirmed *or* a partial deposit was detected, so funds are never orphaned. (2) **Backend TTL sweep**`cancelStaleUnpaidRequests` runs every 5 min, cancels `pending_payment` requests older than 30 minutes that have **no** active payment (`findActivePaymentForRequest === null`); any active/slow on-chain payment is left untouched. The sweep is the backstop for app-closed cases the frontend unmount can't catch.
**Verification:** backend `npx tsc --noEmit` clean; frontend `npx tsc --noEmit --ignoreDeprecations 6.0` + eslint clean. Admin verifies after deploy: shop checkout → reach payment step → press back without paying → order shows `cancelled` (not lingering pending_payment); a paid/underpaid order is NOT cancelled.
---
### 2026-06-04 — frontend@v2.8.92 — Telegram Mini App: buyer receive-goods/confirm + checkout stepper line fix
**Commits:** frontend only (v2.8.92); backend stays at v2.8.77
**Touched:** `telegram-request-detail-view.tsx` (buyer receive-goods flow), `components/telegram-request-stepper.tsx` (row width cap/centre), `locales/{fa,en,types}.ts` (delivery-code + confirm-receipt strings)
**Why:** (1) The buyer had no way to confirm receipt inside the Mini App — once the seller shipped (status `delivery`, fixed in backend v2.8.77's `sameUser` patch), the buyer was stuck at step 5 «دریافت کالا» with no action. Mirrored the web `step-5-receive-goods`: on `status === 'delivery'` for a buyer, auto-load/generate the delivery code (`getDeliveryCode` → fallback `generateDeliveryCode`) shown dashed/monospace for the buyer to hand the seller, plus a «تایید دریافت کالا» button calling `confirmDelivery(requestId)` then `refresh()` so the stepper advances to «تایید نهایی». (2) The 3-step checkout stepper stretched each cell across the full Telegram width, leaving a long connector stub past the last dot — capped the steps row at `steps.length * 96` and centred it (`margin: 0 auto`), so both the 3-step checkout and 6-step request steppers render tight, even connectors.
**Verification:** frontend `npx tsc --noEmit --ignoreDeprecations 6.0` clean. Admin verifies after deploy: buyer opens a `delivery`-status request → sees delivery code + «تایید دریافت کالا» → tap → request advances to تایید نهایی; checkout stepper line is clean (no stub).
---
### 2026-06-03 — scanner@ccd96e8, backend@22ae0bd, frontend@10c4292 — direct scanner balance checks and balance watches
**Commits:** scanner `ccd96e8` (tag `v0.1.8`), backend `22ae0bd` (tag `v2.8.60`), frontend `10c4292` (tag `v2.8.60`)
@@ -699,6 +816,19 @@ TON-only, no EVM; backend already has tonProofService).
---
### 2026-06-03 — frontend@HEAD — Mini App floating cart FAB (v2.8.60)
**Why:** User wants the web shop's small edge-docked basket (count badge) in the Mini App too.
New `TelegramCartFab` component pinned above the tab bar, shown on shop tab + in-shell seller
shop, opens the in-shell cart. Replaced the header cart chip + bottom cart bar.
**Pending next (user requests):** (1) Mini App «تنظیمات» — dark/light mode for the Telegram shell
(needs a dark palette variant of telegram-shell-css + toggle; should follow webApp.colorScheme);
(2) «درخواست جدید» button font — it is Telegram's NATIVE MainButton, font cannot be customized;
option is replacing it with an in-shell button using the project font.
**Verification:** tsc + eslint clean.
---
### 2026-06-02 — backend@7c4dedf — complete dual-write repos, migrations pipeline, TTL scheduler, address reconciliation
**Commits:** `7c4dedf` (backend v2.8.44), frontend v2.8.44
@@ -709,83 +839,612 @@ TON-only, no EVM; backend already has tonProofService).
---
### 2026-06-04backend@41087c7, frontend@ab18334, deployment@8764fdf — CI/CD infrastructure updates and Telegram Mini App delivery flow
### 2026-06-03frontend@HEAD — Mini App floating support bubble (v2.8.61)
**Commits:** backend `41087c7`, `ed7b960`, `aaa7a04`, `f380f92`, `64792b1`; frontend `ab18334`, `4bc038e`, `81caab5`, `0f3b7c0`, `482d64a`, `f6b1b0e`, `d37a0c0`, `abc5e73`, `0001f22`, `3d7df1a`, `e90659f`, `88fa4da`, `9f90995`, `e8fff89`, `715e143`, `57f8067`, `57bd04a`; deployment `8764fdf`
**Touched:** Backend CI/CD pipeline files, frontend Telegram Mini App components, Docker configurations, payment flow, chat archive, marketplace delivery
**Why:** Major CI/CD updates: mount /opt/escrow-dev for compose, build locally with registry push skip, pin pipelines to linux/arm64 agent with native builder. Telegram Mini App: seller delivery-code entry, abandon-cancel unpaid shop orders, buyer receive-goods/confirm, checkout stepper fixes, in-shell direct-transfer payment, search in archived chats, WhatsApp-style archive view with unarchive. Frontend Docker: raise Node.js heap to 4GB for Next.js build.
**Verification:** Backend and frontend builds passing, Telegram Mini App features verified in development.
**Linked docs updated:** [[Telegram Mini App]], [[CI-CD Pipeline]], [[Payment Flow - DePay & Web3]]
**Why:** Parity with the web dashboard's green floating chat widget. New `TelegramSupportFab`
(edge-docked, green bubble) on home/shop/requests tabs + in-shell seller shop; opens/creates the
support chat. Stacks above the cart FAB in shop contexts.
**Verification:** tsc + eslint clean.
**In progress / queued:** Points & Referral audit (background agent — bugs observed: values render
as "·", Bronze level claims "highest level"); Mini App dark mode (تنظیمات); buyer-parity phase 2
(offers view/accept).
---
### 2026-06-04 — backend@9bb73e2, frontend@35ed72d — security hardening and feature enhancements
### 2026-06-03 — backend@c5d6490 — Points & Referral audit fixes (v2.8.62)
**Commits:** backend `9bb73e2`, `0283e73`, `fb85b38`, `94e2dbe`, `118466d`, `fde1c04`, `c2ae239`, `7810d6b`, `e2aad59`, `64c5d5c`, `62f0af8`; frontend `35ed72d`, `0358af5`, `e25b6b8`, `406810d`, `d64e194`, `131700b`, `0bdb5b3`, `7b3e9ca`, `9bafbbb`, `6dc3918`, `a8ae1e3`, `6adb2e0`, `a18e870`, `583d55a`, `7b949bf`
**Touched:** Delivery code auth gates, TTL sweep for stale purchase requests, marketplace uuid/ObjectId seam, chat archive/unarchive, participant canonicalization, stepper navigation, floating cart/support buttons, product-style template cards
**Why:** Security: use sameUser for buyer auth gates, cancel stale unpaid purchase requests via TTL sweep. Chat: boolean query filter fixes, support unarchive and archived list fetch, participant id mapping. Telegram: seller chat button, notification bell, in-shell shop, account tab parity, role dashboards, live chat, product-style template cards, floating cart, support bubble, checkout CTA.
**Verification:** Backend typecheck and tests passing, frontend TypeScript compilation clean.
**Linked docs updated:** [[Authentication Flow]], [[Chat Flow]], [[Telegram Mini App]], [[Marketplace API]]
**Why (from points audit):** /dashboard/points showed "·" instead of numbers and told Bronze users
they are at "the highest level". Root causes: (1) level boundary off-by-one — seeds define
inclusive maxPoints (Bronze 0999) but PG used `>` and Mongo `<`, so exact-boundary users resolved
to no level; (2) `level_configs` table in Postgres was created but NEVER seeded → zero levels →
null next-level → "highest level" message. Fixes: inclusive comparisons in both repos +
self-seeding of the 5 default levels (برنز/نقره/طلا/پلاتین/الماس) in ensurePostgresLevelConfigSchema.
**Remaining from audit:** frontend points page swallows API errors (shows zeros instead of an
error state); integration ideas: award points on completed escrow, show points in Mini App.
**Verification:** tsc clean, 8 tests green. Admin verifies after deploy: /dashboard/points shows
real numbers and Bronze shows progress toward نقره.
---
### 2026-06-04backend@cd79460, frontend@baa3e52 — security remediation and payment features
### 2026-06-03frontend@131700b — compact floating cart/support FABs (v2.8.63)
**Commits:** backend `cd79460`, `7c6158d`, `bc5b6aa`, `1c51dd8`; frontend `baa3e52`, `2b99404`, `f9c4b0c`, `450625b`, `59b3a67`, `a133fc0`, `93199d4`
**Touched:** Security ownership checks (SEC-001), error message sanitization (SEC-029), PRD tasks remediation (SEC-001..037), security remediation PRD documentation; Telegram: direct-transfer checkout for non-web3, avatar URL fixes, email verify flow, theme/dark mode, settings/addresses in-shell, solar-style icons
**Why:** Complete security audit remediation: reconcile SEC-001 ownership checks with main's sameUser helper, stop leaking raw error messages (SEC-029), remediate P1+P3+P4 security tasks, add comprehensive security remediation PRD. Telegram Mini App: direct-transfer payment option, in-shell settings, addresses management, theme configuration, avatar upload, achievements display.
**Verification:** Security audit tests passing, error messages no longer expose sensitive data.
**Linked docs updated:** [[Security Audit - 2026-05-24]], [[Security Architecture]], [[Telegram Mini App]], [[Payment Flow - DePay & Web3]]
**Why:** User feedback — the two edge-docked FABs were too large. Compacted paddings, icons,
badge, and stack positions.
**Verification:** tsc clean.
---
### 2026-06-03 — backend@c501b2c, frontend@7fe236f — authentication and chat enhancements
### 2026-06-03 — frontend@d64e194 — Mini App points view + tap-to-verify email (v2.8.64)
**Commits:** backend `c501b2c`, `d4e53f4`, `941feee`, `58eb61c`, `4149fcf`, `c5d6490`, `91877ae`, `14d164c`, `8b8c1ae`, `9424395`, `378f8f6`, `14c231e`; frontend `7fe236f`, `7be7e2b`, `4079730`, `5d2113b`, `0888b30`, `8f23cca`, `1cc4212`
**Touched:** Pending-email change flow, referral signup bonus, shop settings, chat participant names, seller ratings, points level boundaries, uuid/legacy id tolerance, roles (guard), chat/notification fixes, user management, admin dashboard repair
**Why:** Authentication: pending-email change flow with referral signup bonus. Chat: populate participant names on Postgres path, canonicalize participant ids, revert canonicalization that broke membership. Shop: compute seller ratings from real published reviews, seller shop lookup tolerant of uuid/legacy id formats. Points: level boundary fixes, accept legacy 24-hex user ids. Roles: add guard user role. Admin: repair user management dashboard, surface backend error messages.
**Verification:** Backend typecheck and marketplace/chat tests passing, frontend TypeScript clean.
**Linked docs updated:** [[Authentication Flow]], [[Chat Flow]], [[User API]], [[Marketplace API]], [[Points API]]
**Why:** Buyer parity — امتیازات و رفرال now in-shell (TelegramPointsView: points cards, level
progress, referral code copy + stats; account-tab row). Email verification from the Mini App:
unverified badge is tappable → sends code → code-entry sheet → verifyProfileEmail → session
refresh. Reuses existing account.ts actions.
**Verification:** tsc + eslint clean. Test after deploy: account tab → امتیازات row; unverified
user → tap red badge → enter emailed code → badge turns green.
---
### 2026-06-03 — backend@a76b07b, frontend@d8763ac — scanner integration and request template fixes
### 2026-06-03 — backend@91877ae — points lookups accept legacy user ids (v2.8.63)
**Commits:** backend `a76b07b`, `fdf63d2`; frontend `d8763ac`, `42fe05e`
**Touched:** Scanner balance watch callbacks, direct-balance ERC-20 pay-in rail (Phase 1+2), request-template validation errors, maxUsage optional field
**Why:** Payment infrastructure: wire scanner balance watch callbacks for real-time balance monitoring, add direct-balance ERC-20 pay-in rail with Phase 1 and 2 implementation. Request templates: surface validation errors in expanded sections and list view, ensure maxUsage is truly optional to fix template creation issues.
**Verification:** Backend payment tests and typecheck passing, scanner integration tests passing.
**Linked docs updated:** [[Scanner Architecture]], [[Payment Flow - Scanner]], [[Scanner API]]
**Why:** Mini App points view (v2.8.64) failed: every user lookup in DrizzlePointsRepo compared
users.id (uuid) against the JWT legacy 24-hex id → uuid cast error → 500. All lookups now match
either format (id::text = $n OR legacy_object_id = $n).
**Verification:** tsc clean, tests green. Test after deploy: Mini App → حساب → امتیازات shows data.
---
### 2026-06-02backend@6724177, frontend@714dfbd — database and request handling improvements
### 2026-06-03frontend@HEAD — remove MainButton, in-tab CTA, sign-in retry (v2.8.65)
**Commits:** backend `6724177`, `f588d52`, `20435d1`, `8dead04`, `795f161`; frontend `714dfbd`, `6fe1328`, `d7a2a86`
**Touched:** Fresh-DB Postgres migrate + seed path, 0013/0014 migrations validation, seeds Postgres-capable, addresses table migration, address_type enum, admin user management, purchase request id/_id tolerance
**Why:** Database: make fresh-DB Postgres migrate and seed path correct for pg-only mode, validate 0013/0014 migrations for fresh drizzle-kit migrate, create addresses table migration and address_type enum, re-key address store by user_id. Requests: tolerate both id and _id in purchase request responses for Mongo-PG compatibility. Admin: make admin user management work end-to-end, unblock user creation and purchase requests for native-PG users.
**Verification:** Backend database migration tests passing, seed scripts working in pg-only mode.
**Linked docs updated:** [[Database Strategy - Mongo vs Postgres Assessment]], [[Postgres Runtime Cutover Status]], [[Address]]
**Why (user requests):** (1) native Telegram MainButton removed — wrong font, duplicated CTAs;
«+ درخواست جدید» button added inside the requests tab header instead. (2) Intermittent 503 on
Telegram sign-in (backend redeploy windows) — sign-in now retries transient 502/503/504 3x with
backoff and shows the Persian message.
**Queued (user, with screenshots):** cart =remove at qty 1; seller-shop card redesign (+ icon,
template detail view, search/filter/sort); requests search + status-chip filters + sort; shop
search; seller chat button; referral signup points; dark mode.
**Verification:** tsc + eslint clean.
---
### 2026-06-02backend@515bea3, frontend@424ce1f — MongoDB removal and CI/CD updates
### 2026-06-03frontend v2.8.6667 — seller chat, header bell, cart UX + review system audit
**Commits:** backend `515bea3`, `4949988`; frontend `424ce1f`, `e33444f`
**Touched:** dataCleanupService guard against MONGO_CONNECT_MODE=never, route admin user counts through postgres-capable stores
**Why:** MongoDB removal: guard dataCleanupService against MONGO_CONNECT_MODE=never to prevent Mongo connections when disabled, route admin user dependency counts through postgres-capable repository stores instead of direct Mongo model access. CI/CD: push to main builds dev-* image and deploys to Arcane dev env, restrict dev pipeline to manual trigger only.
**Verification:** Backend typecheck and repository tests passing, Mongo connectivity guards verified.
**Linked docs updated:** [[MongoDB Removal Handoff]], [[Postgres Runtime Cutover Status]], [[Backend Core Stack Decision Record - 2026-05-24]]
**v2.8.66:** seller rows in the Mini App shop get a chat icon (direct conversation with the
seller); header gets a notification bell opening the in-shell notifications list.
**v2.8.67 fixes:** seller chat gave "Validation failed" (backend expects ONE participant for
direct chats — current user auto-added; we sent two). Cart «−» at qty 1 now removes the item
(separate remove button dropped). Bell: borderless + red dot instead of count.
**Review/rating system audit completed (agent):** backend schema/API/store fully working
(reviews table, /api/marketplace/reviews). Broken: seller rating HARDCODED 4.5 in
DrizzleMarketplaceRepo.findSellersWithActiveRequestTemplates (never reads real reviews);
review form never sends purchaseRequestId (verified-buyer badge never set); Step 6
post-delivery has NO review prompt; Mini App has no reviews. Full phased plan in agent output —
next work items.
---
### 2026-06-03 — backend@80a1e52, frontend@b2d8a32 — integration and payment features
### 2026-06-03 — backend v2.8.6465 — real seller ratings + chat participant names
**Commits:** backend `80a1e52`, `dc6a35f`, `0a0d489`, `e8e8bc9`, `126c222`; frontend `b2d8a32`, `3c9459f`, `51157b0`, `d8763ac`
**Touched:** Merge integrate-main-into-development, correct duplicate version field, telegram user persistence on sign-in, marketplace template creation CHECK constraints, chat support welcome message, in-shell checkout, digital items skip address, stepper fixes
**Why:** Integration: merge main into development branch, correct duplicate version field and bump to v2.8.69. Authentication: persist telegram user updates on sign-in. Marketplace: fix template creation 500 errors from blank optional fields tripping CHECK constraints. Chat: attribute support welcome message to support user. Telegram: in-shell checkout without web hand-off, digital items skip address step, checkout stepper improvements.
**Verification:** Backend and frontend builds passing, integration tests clean.
**Linked docs updated:** [[Integration]], [[Marketplace API]], [[Telegram Mini App]]
**v2.8.64:** seller list ratings now aggregated from published reviews (was hardcoded 4.5);
exposes rating/ratingCount per seller.
**v2.8.65:** PG chats showed "Unknown User" — participants (JSONB ids) are now populated with
firstName/lastName/email/avatar from the users table (uuid OR legacy id matched), in
findByIdWithParticipants and findForUser. Parity with Mongo .populate().
**In progress:** background agent fixing seeds for the PG database + creating complete mock shops
(categories in PG → fixes template-creation 500) — will be reviewed and shipped when it reports.
**Queued (user):** in-shell تنظیمات عمومی (profile editing) in Mini App; post-delivery review
prompt; template detail view + search/filter/sort in shop and requests; dark mode.
---
### 2026-06-03 — backend v2.8.6566 — PG seeds, mock shops, template-creation fix
**Template-creation 500 root cause (agent):** categories table column conflict (Drizzle "order"
vs categoryStore order_num) + random legacy ids per seed → category ids served by the API were
unresolvable by createRequestTemplate. Fixed: categoryStore aligned to canonical "order" column
(with one-time order_num migration), deterministic category legacy ids, hardened categories
fallback (id::text match). Also fixed: the v2.8.64 rating aggregation queried Drizzle reviews
columns but the physical table (written by reviewStore) has different columns — rewritten
against the real columns (seller_user_id).
**New seed:** src/seeds/seedMockShops.ts — Persian categories, 5 sellers + shop settings,
templates with picsum product images, published reviews. npm scripts fixed (paths) + added
seed:levels / seed:templates / seed:mock-shops.
**⚠️ DEV ENV REQUIREMENT:** CATEGORY_STORE=postgres (and ideally REVIEW_STORE /
SHOP_SETTINGS_STORE = postgres) must be set in dev .env.local when REPO_MARKETPLACE=postgres —
docker-compose.dev.yml still defaults these to mongo. Without this, template creation keeps
failing.
**Run on dev after deploy:** docker exec <backend-container> npm run seed:categories && npm run
seed:mock-shops
**Verification:** tsc clean, 14 tests green (3 suites).
---
### 2026-06-03 — frontend v2.8.68 — product-style template cards + in-shell template detail
**Why (user reference design):** seller-shop template cards redesigned image-first (photo +
circular add-to-cart overlay + title/price); tapping opens a new in-shell template detail view
(image gallery with thumbnails/counter, product-type chip, price, description, specs, tags,
sticky add-to-cart/order). New view: telegram-template-detail-view.tsx; shell openTemplate
overlay.
**Dev env (user applied):** CATEGORY_STORE/REVIEW_STORE/SHOP_SETTINGS_STORE=postgres set;
backend v2.8.66 restarted healthy; 24 categories auto-seeded into PG. seed:mock-shops still to
be run by the user for demo data.
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — backend v2.8.67 — auto-seed mock shops on startup
**Feature:** new env flag `SEED_MOCK_SHOPS_ON_START=true` runs seedMockShops() during backend
boot (after migrations/levels). Idempotent — existing rows are skipped — so the flag can stay
on permanently in dev.
**Action for dev env:** add `SEED_MOCK_SHOPS_ON_START=true` next to the other store flags and
restart the backend container; demo shops/templates/reviews appear automatically.
**Verification:** tsc clean.
---
### 2026-06-03 — frontend v2.8.69 — web-app-parity shop cards + template detail gallery
**Per user screenshots:** (1) shop cards — circular «+» add-to-cart moved OFF the photo, now
sits beside the title/price row; (2) template detail — web-style carousel: rounded image,
n/m arrow/counter pill, touch-swipe navigation, centered thumbnails; (3) removed «سفارش این
قالب» everywhere (product logic: buyer either creates a request or buys via cart) — add-to-cart
is the single full-width CTA in the detail sticky bar.
Files: telegram-seller-shop-view.tsx, telegram-template-detail-view.tsx.
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — frontend v2.8.70 — in-shell settings + addresses, central theme + dark mode
**In-shell account screens (were opening the web dashboard):**
- تنظیمات عمومی → telegram-settings-view.tsx: name/email/bio edit form via
updateUserProfile; session refresh on save.
- آدرس‌های تحویل → telegram-addresses-view.tsx: full address CRUD (list / add /
edit / delete / set-primary) on the addresses API + use-telegram-addresses SWR hook.
- Account rows switched from href→onClick; new overlay states 'settings'/'addresses'
in the shell. (Passkey stays web — WebAuthn limitation.)
**Theme overhaul — Mini App now uses the CENTRAL web-app theme:**
- constants.ts: TG_PALETTE_LIGHT / TG_PALETTE_DARK derived from
src/theme/theme-config.ts (primary green, grey ramp, status colors) instead of
the ad-hoc cream/saffron palette; font stack aligned with AMANEH_FONT
(IBM Plex Sans / Vazirmatn).
- telegram-shell-css.ts: emits both palettes; `.tg-shell--dark` flips every token —
components keep the same CSS-var names.
- use-telegram-theme hook: auto (follow Telegram colorScheme) | light | dark,
persisted in localStorage; syncs Telegram native header/background colors.
- Theme toggle row (auto/light/dark) added to the account tab.
- Hero headers (account / seller-shop / welcome banner / unlinked) use new
--header-bg/--header-fg/--header-fg-muted vars so they stay legible in both modes.
- Removed the inline themeParams background/text override in getTelegramShellStyle
that forced a navy shell under light components on dark-mode Telegram clients
(the bad bg/font the user reported).
**Verification:** tsc + eslint clean (one pre-existing no-constant-condition
warning in telegram-webapp.ts, untouched).
---
### 2026-06-03 — frontend v2.8.71 — Mini App: solar icons, avatar upload, achievements, theme-toggle fix
**Icons:** telegram-icons.tsx redrawn to mirror the dashboard's Solar "linear"
set (home-2 / document-add / chat-round-dots / user / shop-2 / wallet-money /
rounded arrows). Added sun, moon, themeAuto, camera, trophy, lock.
**Theme toggle now icon-based** (sun=light, moon=dark, half-disc=auto) instead
of text labels — per user request.
**BUGFIX — theme toggle didn't respond:** TelegramListRow rendered a *disabled*
<button> wrapper whenever it had no onClick; a disabled button swallows clicks
from nested children, so the language AND theme toggles (rendered inside the
row's `value` slot) never received taps. Non-interactive rows (no onClick, no
href) now render a <div>.
**Avatar upload in settings:** telegram-settings-view gained a tappable avatar
with a camera badge → file picker → uploadUserAvatar + updateUserProfile
(persists immediately + session refresh); 3 MB / JPG-PNG-GIF validation; relative
/uploads paths resolved to full backend URL.
**Achievements in points view:** milestone badges (joined / verify email / first
referral / 5 active referrals / 1000 points) derived client-side from the user's
points + referral + isEmailVerified data; unlocked (trophy + "earned") vs locked
(lock + "+N" reward) with an N/total counter. Display-layer gamification over the
existing server-side point grants.
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — frontend v2.8.72 — Mini App fixes (avatar/email/web-links) + search/sort
**Bug fixes (user testing on v2.8.71):**
- **Avatar broken in account header** — it rendered the raw relative `/uploads`
path against the frontend origin (404 inside the Telegram WebView). New shared
`resolveTelegramAvatar` (sections/telegram/avatar-url.ts) rebases avatars onto
the backend origin; used in both the account header and the settings uploader.
- **Inline email verification in settings** — changing the email now surfaces a
code field + verify/resend buttons (resendProfileEmailVerification +
verifyProfileEmail) whenever the saved email is unverified, instead of forcing
the user back to the account-tab badge.
- **Passkey/Wallet rows broke the Mini App** — `href` replaced the whole WebView
with the web dashboard and the Telegram back button couldn't return. They now
open in Telegram's in-app browser via `webApp.openLink`
(openTelegramExternalLink in telegram-webapp.ts), keeping the Mini App mounted.
**Feature — search / filter / sort (queued item):**
- Requests: search + status filter chips (all / active / completed / cancelled)
+ sort (newest / highest budget).
- Shop sellers: search + sort (top rated / most templates / newest).
- Seller-shop templates: search + sort (newest / cheapest / priciest), shown
when the shop has > 3 templates.
- New reusable `TelegramListControls` component + `controls` locale section.
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — backend v2.8.68 + frontend v2.8.73 — pending-email flow, referral signup bonus, Mini App cleanups
**Backend v2.8.68 — email change now requires verification before it applies:**
- New `pendingEmail` field. Changing email parks the new address in pendingEmail
(NOT email) and sends a code to the NEW address; the account email + its
verified status stay intact until the code is confirmed, then pendingEmail is
promoted to email (with a re-check nobody else claimed it). Resend targets the
pending address. Implemented in the Mongoose User model AND the Postgres
authStore path (PgAuthUser field + constructor + toObject + savePgUser
INSERT/ON CONFLICT params + mirrorUserToMongo + `pending_email` column via
idempotent ALTER in postgresAuthSchema). userController.updateUserProfile /
resendCurrentUserEmailVerification / verifyCurrentUserEmail updated.
- **Referral signup bonus:** referrer gets +50 points the moment someone signs
up with their code (email + Google paths) via PointsService.addPoints (source
'referral'), referralStats.totalEarned kept in step, 'referral-reward' socket
event. Best-effort, never blocks signup. Separate from the 2% purchase commission.
**Frontend v2.8.73 — Mini App:**
- Removed the «وضعیت‌های امانت» escrow-states legend from Home.
- Removed the Passkey row from the account tab (not wanted in the Mini App).
- Welcome-banner greeting uses the AMN account name before the Telegram profile
name, so an in-app rename updates it (was stuck on Telegram's "Anonymous …").
- Settings email verify aligned with the pending-email flow: inline code panel
shows whenever a change is pending or the email is unverified; clears on verify.
**Verification:** backend tsc clean; frontend tsc + eslint clean.
---
### 2026-06-03 — frontend v2.8.74 — Mini App chat alignment fix + read-only email field
- **Chat both-sides bug:** in the Mini App chat every bubble rendered on the
same side in one colour. Root cause: `isMine = senderId === selfId` — when
`selfId` was undefined it matched system/welcome messages (no senderId), so
all bubbles looked "mine". Fixed: ids normalised (string | populated object),
a match now requires BOTH ids non-empty, and `selfId` falls back from `_id`
to `id`. Own = right/primary green, support/system = left/neutral grey.
(telegram-chat-bubble.tsx + selfId hardening in telegram-mini-app-view.tsx.)
- **Read-only email:** the email field is now disabled by default with an
explicit «تغییر ایمیل» toggle, in BOTH the Mini App settings and the web
account-general form. Together with the backend pending-email flow (v2.8.68),
email can only change after the code sent to the NEW address is verified —
no silent change by typing + save.
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — frontend v2.8.75 — self-contained email-change flow (visible code entry)
After enabling «تغییر ایمیل» there was no obvious place to send/enter the code.
Reworked into a dedicated flow on both surfaces:
- Mini App settings: editing the email reveals a «ارسال کد تأیید» button beside
it; pressing it parks the new address (pending) + emails the code and
immediately reveals the code-entry panel. Email is no longer bundled into the
name/bio Save (handleRequestEmailChange).
- Web account-general: submitting an email change reveals the verification Alert
(code field + verify/resend) even though the account email stays on the old
verified address until the code is confirmed (pendingEmailVerify state).
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — frontend v2.8.76 — email send-code always reveals the verify panel
Bugfix: «ارسال کد تأیید» was disabled when the email was unchanged and
handleRequestEmailChange early-returned silently for the same address — so after
"sending" nothing appeared. Now the button is enabled whenever an email is
present; a changed email parks it as pending + codes the new inbox, an unchanged
email (re)sends a code to verify the current address. Either way pendingVerify
flips on and the inline code-entry panel renders.
**Verification:** tsc + eslint clean.
---
### 2026-06-03 — frontend v2.8.77 — keep email verify panel mounted after sending code
Bugfix: pressing «ارسال کد تأیید» reverted the settings view to its initial
(non-edit, no-panel) state — the code field never appeared. Root cause:
handleRequestEmailChange awaited onSaved (checkUserSession), which sets the auth
provider's `loading` flag true; the Mini App shell gates the entire linked block
on `!loading`, so it unmounted + remounted the settings view, wiping its local
pendingVerify/emailEditable state. Removed the session refresh from the send-code
path (the email is only pending then — nothing to refresh until verified;
handleVerify still refreshes after a successful confirm).
**Verification:** tsc clean.
---
### 2026-06-03 — backend v2.8.69 + frontend v2.8.78 — chat system messages + post-delivery review
**Chat both-sides (round 2 — real root cause):** support welcome/greeting and
group "chat created" messages are authored on the backend with
`chat.metadata.createdBy` (the buyer) as senderId, so they rendered as the
buyer's own. Backend (v2.8.69): `sendSystemMessage` now takes an optional
senderId and createSupportChat passes the support user's id. Frontend (v2.8.78):
any `messageType:'system'` message renders as a neutral centred note regardless
of sender — robust fix for all system messages.
**Post-delivery seller review (frontend v2.8.78):** buyer viewing a
delivery/delivered/seller_paid/completed request sees a star + comment prompt
(TelegramReviewPrompt). Seller resolved from the request's selected offer
(getOffers → selectedOfferId → sellerId); submits createReview({subjectType:
'seller', subjectId, rating, comment, purchaseRequestId}); 409 → "already
reviewed" handled gracefully.
**Verification:** backend + frontend tsc clean.
---
### 2026-06-03 — backend v2.8.70 — persist telegram user updates on sign-in (new-user login)
Investigating "new Telegram account / new device can't log in (existing works)".
telegramAuth mutates the user after the TelegramLink lookup (telegramVerified
flip, lazily backfilled referralCode, lastLoginAt) but only NEW users hit
user.save(); existing/half-created users were never re-persisted → a backfilled
referralCode never landed and subsequent sign-ins kept tripping the non-sparse
users.referralCode index. Added a best-effort user.save() before linking.
**Note:** background logs in the user's screenshots show "Backend 2.6.49" — the
deployed dev backend may be far behind (none of the 2.8.x auth/email/chat fixes
live). Asked the user to confirm/redeploy the backend and share the exact error
the new device shows (frontend surfaces backend message verbatim).
**Verification:** backend tsc clean.
---
### 2026-06-03 — backend v2.8.71 — fix template-creation 500 (blank optional fields vs CHECK constraints)
Seller "create request template" returned HTTP 500 whenever optional fields were
left empty. The request_templates table has CHECK constraints that allow only
NULL or a valid pattern — product_link (`~ '^https?://.+'`) and delivery_email
(email regex) — and the nullable service_session_type enum rejects ''. Both
DrizzleMarketplaceRepo.createRequestTemplate and updateRequestTemplate passed the
form's empty strings straight into the INSERT/UPDATE, tripping the constraints.
Added a `blank()` coercion: blank optional strings → NULL/undefined for
product_link, delivery_email, size/color/brand, service_location/notes,
proposal_title/description, service_session_type; enum columns (product_type,
delivery_type, urgency, budget_currency, metadata_source) fall back to their
defaults instead of receiving ''.
(Confirmed deployed backend is now on the 2.8.x line — logs show v2.8.70.)
**Verification:** backend tsc clean.
---
### 2026-06-03 — frontend v2.8.79 — request-template maxUsage truly optional
The «حداکثر تعداد استفاده (اختیاری)» field threw a ZodError ("Invalid input") and
blocked the template form submit. It was wrapped in schemaHelper.nullableInput —
a helper for REQUIRED fields that adds an issue on null/undefined — so an empty
optional value failed. Replaced with zod.preprocess (blank/null/NaN → undefined,
validate int ≥ 1 only when present); default null → undefined. Together with
backend v2.8.71 (blank → NULL for product_link/delivery_email/enums), optional
template fields can now be left empty end-to-end.
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — backend v2.8.72→v2.8.73 (chat participant fix reverted) + frontend v2.8.80 (template validation visibility)
**Backend chat — attempted + reverted.** v2.8.72 tried to canonicalize chat
participant ids (userRepo.findById) so a buyer (marketplace uuid for the seller)
and seller (legacy ObjectId session) would share one direct thread. It backfired:
findById returns an id format (Mongo ObjectId / PG uuid) that no longer matches
the session JWT id, so the membership check rejected everyone ("User is not a
participant"). v2.8.73 reverts to storing ids as passed (restores chat access).
**Open issue:** buyer↔seller messages still don't cross — they land in separate
direct conversations because the seller is referenced by a uuid while the session
id is a legacy ObjectId (users in Mongo, chats in Postgres). Needs a correct
uuid→legacyObjectId resolution (via marketplace id_map), not the session-breaking
findById path. Deferred for a careful fix.
**Frontend v2.8.80:** template-form onInvalid now expands all collapsed accordion
sections and toasts the actual nested error messages, so validation errors are
visible under their fields instead of only in the console.
---
### 2026-06-04 — backend v2.8.74 — chat: map uuid participants → session ObjectId (buyer↔seller fix)
Root cause confirmed from logs: a buyer opens a chat "with seller X" using X's
marketplace **Postgres uuid** (669c0dac-…), but X's session/JWT id is their
**legacy ObjectId** (e0527…). Conversations key participants by the session id,
so buyer and seller landed in two separate direct threads (each saw only their
own messages). ChatService.createChat now normalises any uuid participant (and
createdBy) → users.legacy_object_id via a direct PG lookup BEFORE dedup/create —
resolving TOWARD the session-id format (the reverted v2.8.72 resolved toward uuid
and broke membership). Both sides now converge on one conversation + socket room.
Note: pre-existing mismatched conversations are orphaned; a freshly opened chat
dedups correctly for both parties.
**Verification:** backend tsc clean.
---
### 2026-06-04 — frontend v2.8.81 — in-shell checkout (Mini App no longer hands off to web)
Cart «ادامه و پرداخت» previously navigated the Telegram WebView to the web
checkout (taking over the Mini App). New TelegramCheckoutView keeps it in-shell:
order summary + delivery-address picker (reuses saved addresses; "add address"
opens the in-shell addresses manager) → places the order via
convertTemplatesToRequests({paymentConfirmed:false}) so requests are created in
`pending_payment` (escrow model — NO on-chain/web3 transaction needed inside the
Mini App). Cart cleared on success and the shell jumps to Requests, where the
buyer completes the escrow payment. New 'checkout' shell overlay + cart `clear()`
on the cart hook. Verified the deferred path exists (status 'pending_payment'
when paymentConfirmed is false) rather than requiring batch-convert's
paymentConfirmed:true on-chain flow.
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — frontend v2.8.82 — checkout: digital skips address; fix buyer pending_payment stepper
- In-shell checkout: only PHYSICAL products require a delivery address; digital
products / services / consultations are delivered online (email/link) so the
address picker is hidden. Cart items now carry productType; needsAddress is
derived from it (deliveryType fallback for older items).
- Buyer stepper fix (request-config determineBuyerStep): pending_payment /
payment / payment_pending were grouped with the PAID states and returned step
4 ("awaiting shipment"), so a just-placed unpaid order looked like the payment
step was skipped. Those un-paid statuses now return step 3 ("select & pay"),
so the request correctly shows the payment step as current.
**Open:** the actual on-chain escrow payment step inside the Mini App still needs
a wallet rail (TON Connect / WalletConnect) — order placement is in-shell, the
pay action is the remaining web3 piece.
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — frontend v2.8.83 — Mini App pay CTA (wallet + manual deposit) + stepper line fix
- Request detail: buyers on a payable status (pending_payment/payment) get a
«پرداخت با کیف پول» button that opens the web payment page for that request in
Telegram's in-app browser (openTelegramExternalLink). That page already offers
BOTH WalletConnect/wagmi AND the in-house manual deposit-address (backend
auto-detects the on-chain transfer via request-network webhook), so the user
pays without the Mini App being torn down. The generic "open in web" link also
uses the in-app browser now.
- Stepper green line "broken" fix: the two half-connectors per cell left a seam,
and the active dot's saffron halo overlapped the line. Replaced with one
continuous connector per gap (green once the step is reached) and a solid
paper ring on the active dot — the progress line is now continuous.
**Note:** this reuses the existing web WalletConnect + deposit flow via the
in-app browser; a fully native in-shell wallet/deposit screen remains a larger
follow-up.
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — frontend v2.8.84 — archive conversations from the chat list (DEPLOY BRANCH NOW main)
⚠️ Deploy branch changed: pushes now go to `main` (CI: "push to main builds dev-*
image and deploys to Arcane dev env"). integrate-main-into-development is retired.
- Chat list: each row gained an archive button → confirm sheet → PATCH
/chat/:id/archive (archiveConversation) + list refresh. Archived chats are
excluded from the default getUserChats query, so the row disappears. Row
restructured from a single <button> into a div wrapping the open-button + a
sibling archive button (nested buttons are invalid HTML). New archive icon +
chat archive locale strings.
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — backend v2.8.75 + frontend v2.8.85/86 — chat search + archive view, template→checkout fix
**backend v2.8.75:** archiveChat is now two-way — PATCH /chat/:id/archive accepts
`{archived}` (default true); archived:false restores. Powers unarchive.
**frontend v2.8.85:** template detail — once an item is in the cart the sticky bar
shows «حذف از سبد» + a primary «مشاهدهٔ سبد و پرداخت» (onOpenCart) so the buyer
can actually reach checkout (previously the single button just toggled).
**frontend v2.8.86:** chat list gained a search box (counterpart name + last
message) and a «گفتگوهای آرشیوشده» row → TelegramArchivedChatsView (lazy
isArchived=true fetch; per-row unarchive via archiveConversation(id,false)).
getConversations/archiveConversation actions + useTelegramConversations(archived)
updated.
All on the new `main` deploy branch. **Verification:** tsc + eslint clean.
---
### 2026-06-04 — backend v2.8.76 + frontend v2.8.87 — fix archive filter (boolean) + archived-view search
**Root cause of "archived chats not removed from main + archived view shows
everything":** DrizzleChatRepo.matchesCondition routed BOOLEAN query conditions
through idEquals, and asStringId() coerces both true and false to '' — so
idEquals(false,true) === true, making the `settings.isArchived` filter match
every chat. Booleans now compare strictly (Boolean(value) === condition); a
missing value counts as false. (Also corrects participants.isActive matching.)
**frontend v2.8.87:** the archived chats view gained the same search box as the
main list (+ no-results state). Unarchive already worked (archiveConversation
(id,false)).
**Verification:** backend + frontend tsc/eslint clean.
---
### 2026-06-04 — frontend v2.8.88 — in-shell direct-transfer payment (no wallet connection)
Buyers can now pay a pending_payment request entirely inside the Mini App, with
NO web3 wallet connection — mirroring the web «انتقال مستقیم» rail.
TelegramPaymentView: resolves the selected offer (seller + amount) →
getPaymentOptions for chain/token → createDirectBalanceIntent (provider
amn.scanner, rail direct_balance, mode check) → shows a copyable derived deposit
address + exact amount + a strong "send only this token on this network" warning
→ «پرداخت کردم» calls checkDirectBalancePayment.
Edge cases (all surfaced): OVERPAYMENT accepted (backend delta>=expected →
paid); UNDERPAYMENT shows "received X of Y, send the rest" (paidDelta vs
expectedAmount); WRONG TOKEN — the check flow can't detect a foreign token, so a
prominent on-screen warning is shown (backend webhook rejects token/chain
mismatch). Wired from the request-detail pay CTA via a new openPaymentRequestId
shell overlay + BackButton. (Replaces the earlier "open web payment" hand-off.)
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — frontend v2.8.89 — Mini App checkout flows straight into payment (shop model)
Per the shop model (pay as part of ordering, not place-then-pay-later), the
in-shell checkout now, after «ادامه و پرداخت», creates the request(s) (template
conversion already mints + accepts an offer and sets selectedOfferId, status
pending_payment) and for a SINGLE-request order jumps directly into the in-shell
direct-transfer payment step (openPaymentRequestId) rather than dropping the
buyer on the Requests list. Multi-request orders still go to Requests. Checkout
reads the created request id from batch-convert's data[]; button relabeled to
«ادامه و پرداخت». (The normal request→offers→pay escrow flow is unchanged.)
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — frontend v2.8.90 — checkout stepper + paid-step advance fix
- In-shell checkout shows a 3-step progress header (cart → address → payment),
reusing TelegramRequestStepper (new optional `title` prop): cart step 1,
checkout step 2, payment step 3 (only when reached from checkout via the new
`checkoutFlow` prop + `paymentCheckoutFlow` shell state). BackButton steps
checkout→cart and checkout-payment→cart.
- Stepper fix: a PAID request (status 'payment' = escrow funded — confirmed
working end-to-end via the AMN scanner: `[direct-balance] check paid … delta`)
was stuck on step 3 because 'payment' was grouped with the unpaid statuses.
determineBuyerStep now maps 'payment' → 4 (awaiting shipment), 'confirming' →
5; only pending_payment/payment_pending stay on 3. Request-detail «پرداخت»
CTA hidden once status is 'payment'.
**Note:** in-shell direct-transfer payment is confirmed working — a real 0.01
USDC BSC transfer to the derived address was auto-detected and advanced the
request to 'payment'.
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — frontend v2.8.91 — seller stepper: paid/shop request shows "ship goods"
Buyer stepper confirmed correct (paid shop request → step 4 «انتظار ارسال»).
Seller side was wrong: a paid shop (template) purchase showed seller step 1
(«ارسال پیشنهاد») because determineSellerStep gated the step on an
accepted/hasOffer context the seller view couldn't reliably compute (id seam).
Since a funded escrow implies an accepted offer for that seller, post-payment
seller steps are now FIXED/unconditional: pending_payment/payment_pending → 2
(awaiting payment), payment/processing/paid → 3 (ship goods), delivery → 4,
delivered/confirming → 5, seller_paid/completed → 6. Removed the unreliable
`accepted` flag. (Pre-payment states keep the hasOffer 1/2 logic.)
**Verification:** tsc + eslint clean.
---
### 2026-06-04 — backend v2.8.77 — seller can't ship: delivery 403 (uuid↔ObjectId seam)
The selected seller got HTTP 403 on PUT /marketplace/purchase-requests/:id/delivery
(«تأیید ارسال کالا») for a paid shop request. The auth compared the seller's
session id (legacy ObjectId e0527…) against the selected offer's sellerId, which a
TEMPLATE-created offer stores as a PG uuid (669c0dac…) → never equal → 403. Added a
`sameUser(a,b)` helper that resolves both ids via the user repo (accepts either
format) and compares every id form (_id/id/pgId/legacyObjectId). Applied to
updateDeliveryInfo, verifyDeliveryCode, and the delivery-code-status buyer/seller
gate. (Buyer step labels already matched the web — no change.)
**Verification:** backend tsc clean.
---