Skip to content

Commit 3df1e46

Browse files
(SP: 3) [SHOP] Launch readiness hardening after repeat audit (#442)
1 parent b1a61ba commit 3df1e46

File tree

85 files changed

+9142
-1370
lines changed

Some content is hidden

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

85 files changed

+9142
-1370
lines changed

frontend/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ GMAIL_USER=
162162
# Set explicitly in production to avoid incorrect absolute URLs.
163163
SHOP_BASE_URL=
164164

165+
# Optional public seller address used by the Shop seller-information legal page.
166+
# Leave empty to keep the current placeholder-based public rendering until launch data is finalized.
167+
SHOP_SELLER_ADDRESS=
168+
165169
# Policy/consent version labels used by Shop flows.
166170
SHOP_PRIVACY_VERSION=privacy-v1
167171
SHOP_TERMS_VERSION=terms-v1

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getMessages, getTranslations } from 'next-intl/server';
55

66
import { AddToCartButton } from '@/components/shop/AddToCartButton';
77
import { ProductGallery } from '@/components/shop/ProductGallery';
8+
import { SizeGuideAccordion } from '@/components/shop/SizeGuideAccordion';
89
import { Link } from '@/i18n/routing';
910
import { getStorefrontAvailabilityState } from '@/lib/shop/availability';
1011
import { formatMoney } from '@/lib/shop/currency';
@@ -39,7 +40,7 @@ export default async function ProductPage({
3940
const commerceProduct =
4041
result.kind === 'available' ? result.commerceProduct : null;
4142
const availabilityState = getStorefrontAvailabilityState(commerceProduct);
42-
const sizeGuide = getApparelSizeGuideForProduct(commerceProduct, locale);
43+
const sizeGuide = getApparelSizeGuideForProduct(product, locale);
4344
const galleryImages = getProductGalleryImages(product);
4445

4546
const NAV_LINK = cn(
@@ -146,6 +147,12 @@ export default async function ProductPage({
146147
);
147148
})()}
148149

150+
{commerceProduct === null && sizeGuide ? (
151+
<section className="mt-6" aria-label="Size guide">
152+
<SizeGuideAccordion sizeGuide={sizeGuide} />
153+
</section>
154+
) : null}
155+
149156
{commerceProduct ? (
150157
<section aria-label="Purchase">
151158
<AddToCartButton

frontend/app/api/shop/admin/orders/[id]/cancel-payment/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { cancelMonobankUnpaidPayment } from '@/lib/services/orders/monobank-canc
2020
import { writeAdminAudit } from '@/lib/services/shop/events/write-admin-audit';
2121
import { orderIdParamSchema, orderSummarySchema } from '@/lib/validation/shop';
2222

23+
export const runtime = 'nodejs';
24+
2325
function noStoreJson(body: unknown, init?: { status?: number }) {
2426
const res = NextResponse.json(body, { status: init?.status ?? 200 });
2527
res.headers.set('Cache-Control', 'no-store');

frontend/app/api/shop/admin/orders/[id]/refund/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { refundOrder } from '@/lib/services/orders';
3030
import { writeAdminAudit } from '@/lib/services/shop/events/write-admin-audit';
3131
import { orderIdParamSchema, orderSummarySchema } from '@/lib/validation/shop';
3232

33+
export const runtime = 'nodejs';
34+
3335
function noStoreJson(body: unknown, init?: { status?: number }) {
3436
const res = NextResponse.json(body, { status: init?.status ?? 200 });
3537
res.headers.set('Cache-Control', 'no-store');

frontend/app/api/shop/admin/products/[id]/status/route.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ProductNotFoundError } from '@/lib/errors/products';
1313
import { logError, logWarn } from '@/lib/logging';
1414
import { requireAdminCsrf } from '@/lib/security/admin-csrf';
1515
import { guardBrowserSameOrigin } from '@/lib/security/origin';
16+
import { InvalidPayloadError, PriceConfigError } from '@/lib/services/errors';
1617
import { toggleProductStatus } from '@/lib/services/products';
1718
import { writeAdminAudit } from '@/lib/services/shop/events/write-admin-audit';
1819

@@ -174,6 +175,47 @@ export async function PATCH(
174175
);
175176
}
176177

178+
if (error instanceof PriceConfigError) {
179+
logWarn('admin_product_status_price_config_error', {
180+
...baseMeta,
181+
code: error.code,
182+
productId: productIdForLog,
183+
currency: error.currency,
184+
durationMs: Date.now() - startedAtMs,
185+
});
186+
187+
return noStoreJson(
188+
{
189+
error: error.message,
190+
code: error.code,
191+
productId: error.productId,
192+
currency: error.currency,
193+
field: 'prices',
194+
},
195+
{ status: 400 }
196+
);
197+
}
198+
199+
if (error instanceof InvalidPayloadError) {
200+
logWarn('admin_product_status_invalid_payload_error', {
201+
...baseMeta,
202+
code: error.code,
203+
productId: productIdForLog,
204+
field: error.field,
205+
durationMs: Date.now() - startedAtMs,
206+
});
207+
208+
return noStoreJson(
209+
{
210+
error: error.message || 'Invalid product state',
211+
code: error.code,
212+
field: error.field,
213+
details: error.details,
214+
},
215+
{ status: 400 }
216+
);
217+
}
218+
177219
logError('admin_product_status_failed', error, {
178220
...baseMeta,
179221
code: 'ADMIN_PRODUCT_STATUS_FAILED',

0 commit comments

Comments
 (0)