Skip to content

Commit 89c91ab

Browse files
Merge pull request #125 from DevLoversTeam/lso/feat/shop
2 parents 943807d + 241f88b commit 89c91ab

45 files changed

Lines changed: 5257 additions & 2976 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/app/[locale]/shop/admin/products/_components/product-form.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
55

66
import { CATEGORIES, COLORS, PRODUCT_TYPES, SIZES } from '@/lib/config/catalog';
77
import type { ProductAdminInput } from '@/lib/validation/shop';
8+
import { logError } from '@/lib/logging';
89

910
const localSlugify = (input: string): string => {
1011
return input
@@ -344,10 +345,11 @@ export function ProductForm({
344345
const destinationSlug = data.product?.slug ?? slugValue;
345346
router.push(`/shop/products/${destinationSlug}`);
346347
} catch (err) {
347-
console.error(
348-
`Failed to ${mode === 'create' ? 'create' : 'update'} product`,
349-
err
350-
);
348+
logError('admin_product_form_failed', err, {
349+
mode,
350+
productId: productId ?? null,
351+
slug: slugValue,
352+
});
351353
setError(
352354
`Unexpected error while ${
353355
mode === 'create' ? 'creating' : 'updating'

frontend/app/[locale]/shop/admin/products/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { db } from '@/db';
66
import { products, productPrices } from '@/db/schema';
77
import { formatMoney, resolveCurrencyFromLocale } from '@/lib/shop/currency';
88
import { fromDbMoney } from '@/lib/shop/money';
9+
import { logWarn } from '@/lib/logging';
910

1011
function formatDate(value: Date | null, locale: string) {
1112
if (!value) return '-';
@@ -22,7 +23,7 @@ function safeFromDbMoney(
2223
try {
2324
return fromDbMoney(value);
2425
} catch (err) {
25-
console.warn('[admin products] fromDbMoney failed', {
26+
logWarn('admin_products_from_db_money_failed', {
2627
...ctx,
2728
valueType: typeof value,
2829
value,

frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
resolveCurrencyFromLocale,
2323
type CurrencyCode,
2424
} from '@/lib/shop/currency';
25+
import { logError } from '@/lib/logging';
2526

2627
type PaymentFormProps = {
2728
orderId: string;
@@ -111,7 +112,7 @@ function StripePaymentForm({ orderId, locale }: PaymentFormProps) {
111112
});
112113
router.push(next);
113114
} catch (error) {
114-
console.error('Payment confirmation failed', error);
115+
logError('stripe_payment_confirm_failed', error, { orderId });
115116
setErrorMessage('We couldn’t confirm your payment. Please try again.');
116117
router.push(`/shop/checkout/error?orderId=${orderId}`);
117118
} finally {

frontend/app/[locale]/shop/checkout/payment/[orderId]/page.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { orderIdParamSchema } from '@/lib/validation/shop';
88
import { getStripeEnv } from '@/lib/env/stripe';
99
import { createPaymentIntent, retrievePaymentIntent } from '@/lib/psp/stripe';
1010
import { setOrderPaymentIntent } from '@/lib/services/orders';
11+
import { logError } from '@/lib/logging';
1112

1213
function getOrderId(params: { orderId?: string }) {
1314
const parsed = orderIdParamSchema.safeParse({ id: params.orderId ?? '' });
@@ -169,15 +170,11 @@ export default async function PaymentPage(props: PaymentPageProps) {
169170
clientSecret = created.clientSecret;
170171
}
171172
} catch (error) {
172-
console.error(
173-
'Failed to initialize Stripe payment intent',
174-
{
175-
orderId: order.id,
176-
existingPi,
177-
phase,
178-
},
179-
error
180-
);
173+
logError('payment_page_failed', error, {
174+
orderId: order.id,
175+
existingPi,
176+
phase,
177+
});
181178

182179
// Leave clientSecret empty -> UI shows "Payment cannot be initialized"
183180
}
Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
import { NextRequest, NextResponse } from "next/server";
1+
import { NextRequest, NextResponse } from 'next/server';
22

3-
import { MoneyValueError } from "@/db/queries/shop/orders";
4-
import { resolveLocaleAndCurrency } from "@/lib/shop/request-locale";
3+
import { MoneyValueError } from '@/db/queries/shop/orders';
4+
import { resolveLocaleAndCurrency } from '@/lib/shop/request-locale';
55

6-
import { rehydrateCartItems } from "@/lib/services/products";
7-
import { cartRehydratePayloadSchema } from "@/lib/validation/shop";
8-
import { InvalidPayloadError, PriceConfigError } from "@/lib/services/errors";
6+
import { rehydrateCartItems } from '@/lib/services/products';
7+
import { cartRehydratePayloadSchema } from '@/lib/validation/shop';
8+
import { InvalidPayloadError, PriceConfigError } from '@/lib/services/errors';
9+
import { logError } from '@/lib/logging';
910

1011
function normalizeCartPayload(body: unknown) {
11-
if (!body || typeof body !== "object") return body;
12+
if (!body || typeof body !== 'object') return body;
1213
const { items, ...rest } = body as { items?: unknown };
1314

1415
if (!Array.isArray(items)) return body;
1516

1617
return {
1718
...rest,
18-
items: items.map((item) => {
19-
if (!item || typeof item !== "object") return item;
19+
items: items.map(item => {
20+
if (!item || typeof item !== 'object') return item;
2021
const { quantity, ...itemRest } = item as { quantity?: unknown };
2122
const normalizedQuantity =
22-
typeof quantity === "string" && quantity.trim().length > 0
23+
typeof quantity === 'string' && quantity.trim().length > 0
2324
? Number(quantity)
2425
: quantity;
2526

@@ -28,73 +29,72 @@ function normalizeCartPayload(body: unknown) {
2829
};
2930
}
3031

32+
function jsonError(
33+
status: number,
34+
code: string,
35+
message: string,
36+
details?: unknown
37+
) {
38+
return NextResponse.json(
39+
{ error: { code, message, ...(details ? { details } : {}) } },
40+
{ status }
41+
);
42+
}
43+
3144
export async function POST(request: NextRequest) {
3245
let body: unknown;
3346

3447
try {
3548
body = await request.json();
3649
} catch {
37-
return NextResponse.json(
38-
{ error: "Unable to process cart data." },
39-
{ status: 400 }
40-
);
50+
return jsonError(400, 'INVALID_PAYLOAD', 'Unable to process cart data.');
4151
}
4252

4353
const normalizedBody = normalizeCartPayload(body);
4454
const parsedPayload = cartRehydratePayloadSchema.safeParse(normalizedBody);
4555

4656
if (!parsedPayload.success) {
47-
return NextResponse.json(
48-
{ error: "Invalid cart payload", details: parsedPayload.error.format() },
49-
{ status: 400 }
50-
);
57+
return jsonError(400, 'INVALID_PAYLOAD', 'Invalid cart payload', {
58+
issues: parsedPayload.error.format(),
59+
});
5160
}
5261

5362
const { currency } = resolveLocaleAndCurrency(request);
5463

55-
5664
try {
5765
const { items } = parsedPayload.data;
5866
const parsedResult = await rehydrateCartItems(items, currency);
5967
return NextResponse.json(parsedResult);
6068
} catch (error) {
61-
console.error("Cart rehydrate failed", error);
69+
logError('cart_rehydrate_failed', error);
6270

71+
// Missing price for locale currency is a CONTRACT error, not a 422.
6372
if (error instanceof PriceConfigError) {
64-
return NextResponse.json(
65-
{
66-
code: error.code,
67-
message: error.message,
68-
details: { productId: error.productId, currency: error.currency },
69-
},
70-
{ status: 422 }
71-
);
73+
return jsonError(400, error.code, error.message, {
74+
productId: error.productId,
75+
currency: error.currency,
76+
});
7277
}
78+
79+
// DB misconfiguration / invalid stored money: treat as 500 (server fault),
80+
// but keep stable code for diagnostics.
7381
if (error instanceof MoneyValueError) {
74-
return NextResponse.json(
82+
return jsonError(
83+
500,
84+
'PRICE_CONFIG_ERROR',
85+
'Invalid price configuration for one or more products.',
7586
{
76-
code: "PRICE_CONFIG_ERROR",
77-
message: "Invalid price configuration for one or more products.",
78-
details: {
79-
productId: error.productId,
80-
field: error.field,
81-
rawValue: error.rawValue,
82-
},
83-
},
84-
{ status: 500 }
87+
productId: error.productId,
88+
field: error.field,
89+
rawValue: error.rawValue,
90+
}
8591
);
8692
}
8793

8894
if (error instanceof InvalidPayloadError) {
89-
return NextResponse.json(
90-
{ code: error.code, message: error.message },
91-
{ status: 400 }
92-
);
95+
return jsonError(400, error.code, error.message);
9396
}
9497

95-
return NextResponse.json(
96-
{ error: "Unable to rehydrate cart." },
97-
{ status: 500 }
98-
);
98+
return jsonError(500, 'INTERNAL_ERROR', 'Unable to rehydrate cart.');
9999
}
100100
}

frontend/app/api/shop/checkout/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createPaymentIntent, retrievePaymentIntent } from '@/lib/psp/stripe';
1010
import {
1111
InsufficientStockError,
1212
InvalidPayloadError,
13+
InvalidVariantError,
1314
PriceConfigError,
1415
OrderStateInvalidError,
1516
} from '@/lib/services/errors';
@@ -28,6 +29,7 @@ import { type PaymentProvider, type PaymentStatus } from '@/lib/shop/payments';
2829
const EXPECTED_BUSINESS_ERROR_CODES = new Set([
2930
'IDEMPOTENCY_CONFLICT',
3031
'INVALID_PAYLOAD',
32+
'INVALID_VARIANT',
3133
'INSUFFICIENT_STOCK',
3234
'PRICE_CONFIG_ERROR',
3335
]);
@@ -47,6 +49,7 @@ function isExpectedBusinessError(err: unknown): boolean {
4749
if (err instanceof InvalidPayloadError) return true;
4850
if (err instanceof InsufficientStockError) return true;
4951
if (err instanceof PriceConfigError) return true;
52+
if (err instanceof InvalidVariantError) return true;
5053

5154
return false;
5255
}
@@ -461,6 +464,15 @@ export async function POST(request: NextRequest) {
461464
);
462465
}
463466

467+
if (error instanceof InvalidVariantError) {
468+
return errorResponse(error.code, error.message, 400, {
469+
productId: error.productId,
470+
field: error.field,
471+
value: error.value,
472+
allowed: error.allowed,
473+
});
474+
}
475+
464476
if (error instanceof IdempotencyConflictError) {
465477
return errorResponse(error.code, error.message, 409, error.details);
466478
}

frontend/app/api/shop/internal/orders/restock-stale/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// C:\Users\milka\devlovers.net\frontend\app\api\shop\internal\orders\restock-stale\route.ts
1+
//app\api\shop\internal\orders\restock-stale\route.ts
22
import { NextRequest, NextResponse } from 'next/server';
33
import crypto from 'crypto';
44
import { sql } from 'drizzle-orm';
@@ -10,6 +10,7 @@ import {
1010
} from '@/lib/services/orders';
1111

1212
import { requireInternalJanitorAuth } from '@/lib/auth/internal-janitor';
13+
import { logError } from '@/lib/logging';
1314

1415
export const runtime = 'nodejs';
1516

@@ -422,7 +423,7 @@ export async function POST(request: NextRequest) {
422423
minIntervalSeconds,
423424
});
424425
} catch (e) {
425-
console.error('restock-stale failed', { runId, error: e });
426+
logError('restock_stale_failed', e, { runId });
426427
return NextResponse.json(
427428
{ success: false, code: 'INTERNAL_ERROR' },
428429
{ status: 500 }

0 commit comments

Comments
 (0)