feat: Phase 6 frontend - server-cart core, mock-gateway backed (scoped)
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 <noreply@anthropic.com>
This commit is contained in:
35
src/app/core/cart/models/server-cart.model.ts
Normal file
35
src/app/core/cart/models/server-cart.model.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
11
src/app/core/cart/services/server-cart-gateway.interface.ts
Normal file
11
src/app/core/cart/services/server-cart-gateway.interface.ts
Normal file
@@ -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<ServerCartLine>;
|
||||||
|
updateLine(lineId: string, qty: number): Observable<ServerCartLine>;
|
||||||
|
removeLine(lineId: string): Observable<void>;
|
||||||
|
startCheckout(deliveryOptionId: string, currency: string): Observable<CheckoutSession>;
|
||||||
|
}
|
||||||
9
src/app/core/cart/services/server-cart-gateway.token.ts
Normal file
9
src/app/core/cart/services/server-cart-gateway.token.ts
Normal file
@@ -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<ServerCartGateway>('SERVER_CART_GATEWAY', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => inject(ServerCartLocalGateway),
|
||||||
|
});
|
||||||
66
src/app/core/cart/services/server-cart-local.gateway.ts
Normal file
66
src/app/core/cart/services/server-cart-local.gateway.ts
Normal file
@@ -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<ServerCartLine> {
|
||||||
|
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<ServerCartLine> {
|
||||||
|
const line = this.lines.find(l => l.id === lineId);
|
||||||
|
if (line) {
|
||||||
|
line.qty = qty;
|
||||||
|
}
|
||||||
|
return of(line as ServerCartLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLine(lineId: string): Observable<void> {
|
||||||
|
this.lines = this.lines.filter(l => l.id !== lineId);
|
||||||
|
return of(void 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
startCheckout(deliveryOptionId: string, _currency: string): Observable<CheckoutSession> {
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user