Skip to content

Commit 8310e18

Browse files
liudmylasovetovsViktorSvertoka
authored andcommitted
(SP: 1)[SHOP] remove unsafe dead cart helpers and clean review nits
1 parent f45d4e6 commit 8310e18

5 files changed

Lines changed: 24 additions & 67 deletions

File tree

frontend/docs/shop/checkout-notifications-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This is a launch rule, not a UI preference.
2626

2727
Notification flows depend on a reliable recipient. If a guest order is created
2828
without email, the notification pipeline can fail or dead-letter because there
29-
is no guaranteed recipient identity. :contentReference[oaicite:2]{index=2}
29+
is no guaranteed recipient identity.
3030

3131
The system must not rely only on:
3232

frontend/lib/cart.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import { z } from 'zod';
22

33
import { logWarn } from '@/lib/logging';
44
import { createCartItemKey } from '@/lib/shop/cart-item-key';
5-
import { fromCents } from '@/lib/shop/money';
65
import {
76
type CartClientItem as ValidationCartClientItem,
87
cartClientItemSchema,
98
type CartRehydrateItem,
109
type CartRehydrateResult,
1110
cartRehydrateResultSchema,
12-
type CartRemovedItem,
1311
MAX_QUANTITY_PER_LINE,
1412
} from '@/lib/validation/shop';
1513

@@ -166,44 +164,6 @@ function normalizeItemsForStorage(
166164
}));
167165
}
168166

169-
export function computeSummaryFromItems(
170-
items: CartRehydrateItem[]
171-
): CartSummary {
172-
if (!items.length) {
173-
return {
174-
totalAmountMinor: 0,
175-
totalAmount: 0,
176-
itemCount: 0,
177-
currency: 'USD',
178-
pricingFingerprint: undefined,
179-
};
180-
}
181-
182-
const currency = (items[0]?.currency ?? 'USD') as CartSummary['currency'];
183-
184-
let totalMinor = 0;
185-
let itemCount = 0;
186-
187-
for (const item of items) {
188-
if (item.currency !== currency) {
189-
throw new Error(
190-
`Cart contains mixed currencies (${currency} and ${item.currency}). Clear cart and try again.`
191-
);
192-
}
193-
194-
totalMinor += item.lineTotalMinor;
195-
itemCount += item.quantity;
196-
}
197-
198-
return {
199-
totalAmountMinor: totalMinor,
200-
totalAmount: fromCents(totalMinor),
201-
itemCount,
202-
currency,
203-
pricingFingerprint: undefined,
204-
};
205-
}
206-
207167
function isRecord(value: unknown): value is Record<string, unknown> {
208168
return !!value && typeof value === 'object' && !Array.isArray(value);
209169
}
@@ -307,14 +267,6 @@ export async function rehydrateCart(
307267
return parsed;
308268
}
309269

310-
export function buildCartFromItems(
311-
items: CartRehydrateItem[],
312-
removed: CartRemovedItem[] = []
313-
): Cart {
314-
const summary = computeSummaryFromItems(items);
315-
return { items, removed, summary };
316-
}
317-
318270
export function clearStoredCart(ownerId?: string | null): void {
319271
if (typeof window === 'undefined') return;
320272

frontend/lib/services/shop/shipping/checkout-quote.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function readNonNegativeIntEnv(name: string): number | null {
4848
if (!/^\d+$/.test(trimmed)) return null;
4949

5050
const parsed = Number.parseInt(trimmed, 10);
51-
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
51+
if (!Number.isSafeInteger(parsed)) return null;
5252

5353
return parsed;
5454
}

frontend/lib/tests/shop/checkout-price-change-fail-closed.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ afterAll(async () => {
133133

134134
async function seedProduct(priceMinor: number): Promise<string> {
135135
const slug = `checkout-price-change-${crypto.randomUUID()}`;
136+
const price = (priceMinor / 100).toFixed(2);
136137

137138
const [product] = await db
138139
.insert(products)
@@ -142,7 +143,7 @@ async function seedProduct(priceMinor: number): Promise<string> {
142143
description: null,
143144
imageUrl: 'https://example.com/checkout-price-change.png',
144145
imagePublicId: null,
145-
price: '9.00',
146+
price,
146147
originalPrice: null,
147148
currency: 'USD',
148149
category: null,
@@ -164,7 +165,7 @@ async function seedProduct(priceMinor: number): Promise<string> {
164165
currency: 'USD',
165166
priceMinor,
166167
originalPriceMinor: null,
167-
price: (priceMinor / 100).toFixed(2),
168+
price,
168169
originalPrice: null,
169170
});
170171

@@ -309,8 +310,7 @@ describe('checkout fail-closed for changed price mismatch', () => {
309310
expect(json.success).toBe(true);
310311
expect(json.order?.totalAmount).toBe(9);
311312

312-
const orderId =
313-
typeof json.order?.id === 'string' ? String(json.order.id) : null;
313+
const orderId = typeof json.order?.id === 'string' ? json.order.id : null;
314314
expect(orderId).toBeTruthy();
315315

316316
if (orderId) {

frontend/lib/tests/shop/checkout-shipping-authoritative-total.test.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import crypto from 'node:crypto';
22

3-
import { eq, inArray } from 'drizzle-orm';
3+
import { eq, inArray, type InferInsertModel } from 'drizzle-orm';
44
import { NextRequest, NextResponse } from 'next/server';
55
import {
66
afterAll,
@@ -91,6 +91,10 @@ type SeedData = {
9191
cityRef: string;
9292
warehouseRef: string;
9393
};
94+
type ProductInsert = InferInsertModel<typeof products>;
95+
type ProductPriceInsert = InferInsertModel<typeof productPrices>;
96+
type NovaPoshtaCityInsert = InferInsertModel<typeof npCities>;
97+
type NovaPoshtaWarehouseInsert = InferInsertModel<typeof npWarehouses>;
9498

9599
beforeAll(async () => {
96100
const checkoutRoute = await import('@/app/api/shop/checkout/route');
@@ -168,8 +172,7 @@ async function seedShippingCheckoutData(): Promise<SeedData> {
168172
const productId = crypto.randomUUID();
169173
const cityRef = crypto.randomUUID();
170174
const warehouseRef = crypto.randomUUID();
171-
172-
await db.insert(products).values({
175+
const productRow: ProductInsert = {
173176
id: productId,
174177
slug: `checkout-shipping-total-${productId.slice(0, 8)}`,
175178
title: 'Checkout Shipping Total Test Product',
@@ -188,29 +191,26 @@ async function seedShippingCheckoutData(): Promise<SeedData> {
188191
isFeatured: false,
189192
stock: 25,
190193
sku: null,
191-
} as any);
192-
193-
await db.insert(productPrices).values({
194+
};
195+
const productPriceRow: ProductPriceInsert = {
194196
id: crypto.randomUUID(),
195197
productId,
196198
currency: 'UAH',
197199
priceMinor: 4000,
198200
originalPriceMinor: null,
199201
price: '40.00',
200202
originalPrice: null,
201-
} as any);
202-
203-
await db.insert(npCities).values({
203+
};
204+
const cityRow: NovaPoshtaCityInsert = {
204205
ref: cityRef,
205206
nameUa: 'Kyiv',
206207
nameRu: 'Kiev',
207208
area: 'Kyivska',
208209
region: 'Kyiv',
209210
settlementType: 'City',
210211
isActive: true,
211-
} as any);
212-
213-
await db.insert(npWarehouses).values({
212+
};
213+
const warehouseRow: NovaPoshtaWarehouseInsert = {
214214
ref: warehouseRef,
215215
cityRef,
216216
settlementRef: cityRef,
@@ -220,7 +220,12 @@ async function seedShippingCheckoutData(): Promise<SeedData> {
220220
address: 'Address 1',
221221
isPostMachine: false,
222222
isActive: true,
223-
} as any);
223+
};
224+
225+
await db.insert(products).values(productRow);
226+
await db.insert(productPrices).values(productPriceRow);
227+
await db.insert(npCities).values(cityRow);
228+
await db.insert(npWarehouses).values(warehouseRow);
224229

225230
createdProductIds.push(productId);
226231
createdCityRefs.push(cityRef);

0 commit comments

Comments
 (0)