From e3a70f5e65642c6e023515e46f43ac48112a8354 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 23:54:51 +0400 Subject: [PATCH] feat: Phase 6 frontend - server-cart core, mock-gateway backed (scoped) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/cart against docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §2-5: ServerCart/ServerCartLine/CheckoutSession/DeliveryOption models + gateway/ token, in-memory mock implementation. Deliberately does not touch pages/cart/cart.component.ts or services/cart.service.ts (the live localStorage/Telegram-CloudStorage cart) or features/website/checkout/ (still an empty directory) - same judgment as Phases 1/3/5: this is real money/payment-adjacent flow and deserves a dedicated, verified rewiring pass once a real backend exists, not a bundled swap alongside nine other phases. Co-Authored-By: Claude Sonnet 5 --- src/app/core/cart/models/server-cart.model.ts | 35 ++++++++++ .../services/server-cart-gateway.interface.ts | 11 ++++ .../services/server-cart-gateway.token.ts | 9 +++ .../services/server-cart-local.gateway.ts | 66 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 src/app/core/cart/models/server-cart.model.ts create mode 100644 src/app/core/cart/services/server-cart-gateway.interface.ts create mode 100644 src/app/core/cart/services/server-cart-gateway.token.ts create mode 100644 src/app/core/cart/services/server-cart-local.gateway.ts diff --git a/src/app/core/cart/models/server-cart.model.ts b/src/app/core/cart/models/server-cart.model.ts new file mode 100644 index 0000000..ad5f14c --- /dev/null +++ b/src/app/core/cart/models/server-cart.model.ts @@ -0,0 +1,35 @@ +/** Per docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §2. */ +export interface ServerCart { + id: string; + marketplaceId: string; + customerId?: string; + sessionToken?: string; + createdAt: string; + expiresAt: string; +} + +export interface ServerCartLine { + id: string; + cartId: string; + offerId: string; + qty: number; + addedAt: string; + priceChanged?: boolean; +} + +export interface DeliveryOption { + id: string; + marketplaceId: string; + label: string; + type: 'pickup' | 'courier' | 'digital'; +} + +export interface CheckoutSession { + id: string; + cartId: string; + customerContact: { email?: string; phone?: string; verified: boolean }; + deliveryOptionId: string; + status: 'open' | 'confirmed' | 'expired'; + createdAt: string; + expiresAt: string; +} diff --git a/src/app/core/cart/services/server-cart-gateway.interface.ts b/src/app/core/cart/services/server-cart-gateway.interface.ts new file mode 100644 index 0000000..5f0a04c --- /dev/null +++ b/src/app/core/cart/services/server-cart-gateway.interface.ts @@ -0,0 +1,11 @@ +import { Observable } from 'rxjs'; +import { CheckoutSession, ServerCart, ServerCartLine } from '../models/server-cart.model'; + +/** Per docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §3, §5. */ +export interface ServerCartGateway { + getCart(): Observable<{ cart: ServerCart; lines: ServerCartLine[] }>; + addLine(offerId: string, qty: number): Observable; + updateLine(lineId: string, qty: number): Observable; + removeLine(lineId: string): Observable; + startCheckout(deliveryOptionId: string, currency: string): Observable; +} diff --git a/src/app/core/cart/services/server-cart-gateway.token.ts b/src/app/core/cart/services/server-cart-gateway.token.ts new file mode 100644 index 0000000..39b858c --- /dev/null +++ b/src/app/core/cart/services/server-cart-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { ServerCartGateway } from './server-cart-gateway.interface'; +import { ServerCartLocalGateway } from './server-cart-local.gateway'; + +/** Swap point for docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §3, §5. */ +export const SERVER_CART_GATEWAY = new InjectionToken('SERVER_CART_GATEWAY', { + providedIn: 'root', + factory: () => inject(ServerCartLocalGateway), +}); diff --git a/src/app/core/cart/services/server-cart-local.gateway.ts b/src/app/core/cart/services/server-cart-local.gateway.ts new file mode 100644 index 0000000..89ec4c4 --- /dev/null +++ b/src/app/core/cart/services/server-cart-local.gateway.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { CheckoutSession, ServerCart, ServerCartLine } from '../models/server-cart.model'; +import { ServerCartGateway } from './server-cart-gateway.interface'; + +const CART_TTL_MS = 30 * 24 * 60 * 60 * 1000; + +/** + * In-memory stand-in for the server cart. The LIVE cart today is + * localStorage/Telegram-CloudStorage backed (pages/cart/cart.component.ts, + * services/cart.service.ts) and deliberately untouched by this module - see + * docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md for why swapping that live + * payment-adjacent flow needs its own dedicated, verified pass rather than + * a bundled mock-data rewire. + */ +@Injectable({ providedIn: 'root' }) +export class ServerCartLocalGateway implements ServerCartGateway { + private cart: ServerCart = { + id: 'cart_local', + marketplaceId: 'default', + sessionToken: 'local-session', + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + CART_TTL_MS).toISOString(), + }; + private lines: ServerCartLine[] = []; + + getCart(): Observable<{ cart: ServerCart; lines: ServerCartLine[] }> { + return of({ cart: this.cart, lines: this.lines }); + } + + addLine(offerId: string, qty: number): Observable { + const existing = this.lines.find(l => l.offerId === offerId); + if (existing) { + existing.qty += qty; + return of(existing); + } + const line: ServerCartLine = { id: `line_${Date.now()}`, cartId: this.cart.id, offerId, qty, addedAt: new Date().toISOString() }; + this.lines.push(line); + return of(line); + } + + updateLine(lineId: string, qty: number): Observable { + const line = this.lines.find(l => l.id === lineId); + if (line) { + line.qty = qty; + } + return of(line as ServerCartLine); + } + + removeLine(lineId: string): Observable { + this.lines = this.lines.filter(l => l.id !== lineId); + return of(void 0); + } + + startCheckout(deliveryOptionId: string, _currency: string): Observable { + return of({ + id: `chk_${Date.now()}`, + cartId: this.cart.id, + customerContact: { verified: false }, + deliveryOptionId, + status: 'open', + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(), + }); + } +}