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