Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion frontend/app/[locale]/admin/shop/orders/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { orderItems, orders } from '@/db/schema';
import { Link } from '@/i18n/routing';
import { getCurrentUser } from '@/lib/auth';
import { logError } from '@/lib/logging';
import {
type CanonicalFulfillmentStage,
deriveCanonicalFulfillmentStage,
latestReturnStatusSql,
latestShipmentStatusSql,
} from '@/lib/services/shop/fulfillment-stage';
import { type CurrencyCode, formatMoney } from '@/lib/shop/currency';
import { fromDbMoney } from '@/lib/shop/money';
import {
Expand Down Expand Up @@ -39,6 +45,7 @@ type OrderDetail = {
paymentStatus: OrderPaymentStatus;
paymentProvider: OrderPaymentProvider;
paymentIntentId: string | null;
fulfillmentStage: CanonicalFulfillmentStage;
shippingStatus: string | null;
trackingNumber: string | null;
stockRestored: boolean;
Expand Down Expand Up @@ -76,6 +83,10 @@ function safeFormatDateTime(iso: string, dtf: Intl.DateTimeFormat): string {
return dtf.format(d);
}

function fulfillmentStageLabelKey(stage: CanonicalFulfillmentStage): string {
return `fulfillmentStages.${stage}`;
}

function toOrderItem(
item: {
id: string | null;
Expand Down Expand Up @@ -150,7 +161,10 @@ export default async function OrderDetailPage({
paymentStatus: orders.paymentStatus,
paymentProvider: orders.paymentProvider,
paymentIntentId: orders.paymentIntentId,
orderStatus: orders.status,
shippingStatus: orders.shippingStatus,
shipmentStatus: latestShipmentStatusSql(orders.id),
returnStatus: latestReturnStatusSql(orders.id),
trackingNumber: orders.trackingNumber,
stockRestored: orders.stockRestored,
restockedAt: orders.restockedAt,
Expand Down Expand Up @@ -183,13 +197,32 @@ export default async function OrderDetailPage({

try {
const base = rows[0]!.order;
const fulfillmentStage = deriveCanonicalFulfillmentStage({
orderStatus: base.orderStatus,
shippingStatus: base.shippingStatus,
shipmentStatus:
typeof base.shipmentStatus === 'string' ? base.shipmentStatus : null,
returnStatus:
typeof base.returnStatus === 'string' ? base.returnStatus : null,
});

const items = rows
.map(r => toOrderItem(r.item))
.filter((i): i is NonNullable<typeof i> => i !== null);

order = {
...base,
id: base.id,
userId: base.userId,
totalAmount: base.totalAmount,
currency: base.currency,
paymentStatus: base.paymentStatus,
paymentProvider: base.paymentProvider,
paymentIntentId: base.paymentIntentId,
fulfillmentStage,
shippingStatus: base.shippingStatus,
trackingNumber: base.trackingNumber,
stockRestored: base.stockRestored,
idempotencyKey: base.idempotencyKey,
createdAt: base.createdAt.toISOString(),
updatedAt: base.updatedAt.toISOString(),
restockedAt: base.restockedAt ? base.restockedAt.toISOString() : null,
Expand Down Expand Up @@ -276,6 +309,15 @@ export default async function OrderDetailPage({
</dd>
</div>

<div>
<dt className="text-muted-foreground text-xs">
{t('fulfillmentStage')}
</dt>
<dd className="text-sm font-medium">
{t(fulfillmentStageLabelKey(order.fulfillmentStage))}
</dd>
</div>

<div>
<dt className="text-muted-foreground text-xs">
{t('shippingStatus')}
Expand Down
44 changes: 43 additions & 1 deletion frontend/app/[locale]/shop/orders/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { orderItems, orders } from '@/db/schema';
import { Link } from '@/i18n/routing';
import { getCurrentUser } from '@/lib/auth';
import { logError } from '@/lib/logging';
import {
type CanonicalFulfillmentStage,
deriveCanonicalFulfillmentStage,
latestReturnStatusSql,
latestShipmentStatusSql,
} from '@/lib/services/shop/fulfillment-stage';
import { type CurrencyCode, formatMoney } from '@/lib/shop/currency';
import { fromDbMoney } from '@/lib/shop/money';
import {
Expand Down Expand Up @@ -40,6 +46,7 @@ type OrderDetail = {
paymentStatus: OrderPaymentStatus;
paymentProvider: OrderPaymentProvider;
paymentIntentId: string | null;
fulfillmentStage: CanonicalFulfillmentStage;
shippingStatus: string | null;
trackingNumber: string | null;
stockRestored: boolean;
Expand Down Expand Up @@ -77,6 +84,10 @@ function safeFormatDateTime(iso: string, dtf: Intl.DateTimeFormat): string {
return dtf.format(d);
}

function fulfillmentStageLabelKey(stage: CanonicalFulfillmentStage): string {
return `fulfillmentStages.${stage}`;
}

function toOrderItem(
item: {
id: string | null;
Expand Down Expand Up @@ -153,7 +164,10 @@ export default async function OrderDetailPage({
paymentStatus: orders.paymentStatus,
paymentProvider: orders.paymentProvider,
paymentIntentId: orders.paymentIntentId,
orderStatus: orders.status,
shippingStatus: orders.shippingStatus,
shipmentStatus: latestShipmentStatusSql(orders.id),
returnStatus: latestReturnStatusSql(orders.id),
trackingNumber: orders.trackingNumber,
stockRestored: orders.stockRestored,
restockedAt: orders.restockedAt,
Expand Down Expand Up @@ -189,13 +203,32 @@ export default async function OrderDetailPage({

try {
const base = rows[0]!.order;
const fulfillmentStage = deriveCanonicalFulfillmentStage({
orderStatus: base.orderStatus,
shippingStatus: base.shippingStatus,
shipmentStatus:
typeof base.shipmentStatus === 'string' ? base.shipmentStatus : null,
returnStatus:
typeof base.returnStatus === 'string' ? base.returnStatus : null,
});

const items = rows
.map(r => toOrderItem(r.item))
.filter((i): i is NonNullable<typeof i> => i !== null);

order = {
...base,
id: base.id,
userId: base.userId,
totalAmount: base.totalAmount,
currency: base.currency,
paymentStatus: base.paymentStatus,
paymentProvider: base.paymentProvider,
paymentIntentId: base.paymentIntentId,
fulfillmentStage,
shippingStatus: base.shippingStatus,
trackingNumber: base.trackingNumber,
stockRestored: base.stockRestored,
idempotencyKey: base.idempotencyKey,
createdAt: base.createdAt.toISOString(),
updatedAt: base.updatedAt.toISOString(),
restockedAt: base.restockedAt ? base.restockedAt.toISOString() : null,
Expand Down Expand Up @@ -282,6 +315,15 @@ export default async function OrderDetailPage({
</dd>
</div>

<div>
<dt className="text-muted-foreground text-xs">
{t('fulfillmentStage')}
</dt>
<dd className="text-sm font-medium">
{t(fulfillmentStageLabelKey(order.fulfillmentStage))}
</dd>
</div>

<div>
<dt className="text-muted-foreground text-xs">
{t('shippingStatus')}
Expand Down
7 changes: 6 additions & 1 deletion frontend/app/api/shop/admin/orders/[id]/shipping/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ export const runtime = 'nodejs';

const payloadSchema = z
.object({
action: z.enum(['retry_label_creation', 'mark_shipped', 'mark_delivered']),
action: z.enum([
'recover_initial_shipment',
'retry_label_creation',
'mark_shipped',
'mark_delivered',
]),
})
.strict();

Expand Down
31 changes: 30 additions & 1 deletion frontend/app/api/shop/orders/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { db } from '@/db';
import { orderItems, orders } from '@/db/schema';
import { getCurrentUser } from '@/lib/auth';
import { logError, logWarn } from '@/lib/logging';
import {
type CanonicalFulfillmentStage,
deriveCanonicalFulfillmentStage,
latestReturnStatusSql,
latestShipmentStatusSql,
} from '@/lib/services/shop/fulfillment-stage';
import { orderIdParamSchema } from '@/lib/validation/shop';

export const dynamic = 'force-dynamic';
Expand All @@ -28,6 +34,7 @@ type OrderDetailResponse = {
paymentStatus: OrderPaymentStatus;
paymentProvider: string;
paymentIntentId: string | null;
fulfillmentStage: CanonicalFulfillmentStage;
shippingStatus: string | null;
trackingNumber: string | null;
stockRestored: boolean;
Expand Down Expand Up @@ -148,7 +155,10 @@ export async function GET(
paymentStatus: orders.paymentStatus,
paymentProvider: orders.paymentProvider,
paymentIntentId: orders.paymentIntentId,
orderStatus: orders.status,
shippingStatus: orders.shippingStatus,
shipmentStatus: latestShipmentStatusSql(orders.id),
returnStatus: latestReturnStatusSql(orders.id),
trackingNumber: orders.trackingNumber,
stockRestored: orders.stockRestored,
restockedAt: orders.restockedAt,
Expand Down Expand Up @@ -184,13 +194,32 @@ export async function GET(
}

const base = rows[0]!.order;
const fulfillmentStage = deriveCanonicalFulfillmentStage({
orderStatus: base.orderStatus,
shippingStatus: base.shippingStatus,
shipmentStatus:
typeof base.shipmentStatus === 'string' ? base.shipmentStatus : null,
returnStatus:
typeof base.returnStatus === 'string' ? base.returnStatus : null,
});

const items = rows
.map(r => toOrderItem(r.item))
.filter((i): i is NonNullable<typeof i> => i !== null);

const response: OrderDetailResponse = {
...base,
id: base.id,
userId: base.userId,
totalAmount: base.totalAmount,
currency: base.currency,
paymentStatus: base.paymentStatus,
paymentProvider: base.paymentProvider,
paymentIntentId: base.paymentIntentId,
fulfillmentStage,
shippingStatus: base.shippingStatus,
trackingNumber: base.trackingNumber,
stockRestored: base.stockRestored,
idempotencyKey: base.idempotencyKey,
createdAt: base.createdAt.toISOString(),
updatedAt: base.updatedAt.toISOString(),
restockedAt: base.restockedAt ? base.restockedAt.toISOString() : null,
Expand Down
23 changes: 22 additions & 1 deletion frontend/app/api/shop/shipping/methods/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import crypto from 'node:crypto';

import { NextRequest, NextResponse } from 'next/server';

import { getShopShippingFlags } from '@/lib/env/nova-poshta';
import {
assertNovaPoshtaProductionLikeReady,
getShopShippingFlags,
NovaPoshtaConfigError,
} from '@/lib/env/nova-poshta';
import { readPositiveIntEnv } from '@/lib/env/readPositiveIntEnv';
import { logError, logWarn } from '@/lib/logging';
import {
Expand Down Expand Up @@ -208,6 +212,23 @@ export async function GET(request: NextRequest) {
);
}

try {
assertNovaPoshtaProductionLikeReady();
} catch (error) {
if (error instanceof NovaPoshtaConfigError) {
return noStoreJson(
{
success: false,
code: 'NP_MISCONFIG',
message: 'Nova Poshta configuration is invalid',
},
requestId,
503
);
}
throw error;
}

return cachedJson(
{
success: true,
Expand Down
55 changes: 54 additions & 1 deletion frontend/lib/env/monobank.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import 'server-only';

import { getRuntimeEnv } from '@/lib/env';
import {
assertProductionLikeProviderString,
assertProductionLikeProviderUrl,
} from '@/lib/env/provider-runtime';

export type MonobankEnv = {
token: string | null;
Expand Down Expand Up @@ -70,6 +74,33 @@ function resolveMonobankToken(): string | null {
return nonEmpty(process.env.MONO_MERCHANT_TOKEN);
}

function assertMonobankRuntimeConfig(args: {
token: string;
apiBaseUrl: string;
publicKey: string | null;
}) {
assertProductionLikeProviderString({
provider: 'monobank',
envName: 'MONO_MERCHANT_TOKEN',
value: args.token,
minLength: 8,
});
assertProductionLikeProviderUrl({
provider: 'monobank',
envName: 'MONO_API_BASE',
value: args.apiBaseUrl,
});

if (args.publicKey) {
assertProductionLikeProviderString({
provider: 'monobank',
envName: 'MONO_PUBLIC_KEY',
value: args.publicKey,
minLength: 8,
});
}
}

function resolveBaseUrlSource(): MonobankConfig['baseUrlSource'] {
if (nonEmpty(process.env.SHOP_BASE_URL)) return 'shop_base_url';
if (nonEmpty(process.env.APP_ORIGIN)) return 'app_origin';
Expand All @@ -82,6 +113,12 @@ export function requireMonobankToken(): string {
if (!token) {
throw new Error('MONO_MERCHANT_TOKEN is required for Monobank operations.');
}
assertMonobankRuntimeConfig({
token,
apiBaseUrl:
nonEmpty(process.env.MONO_API_BASE) ?? 'https://api.monobank.ua',
publicKey: nonEmpty(process.env.MONO_PUBLIC_KEY),
});
return token;
}

Expand Down Expand Up @@ -113,6 +150,12 @@ export function getMonobankEnv(): MonobankEnv {
};
}

assertMonobankRuntimeConfig({
token,
apiBaseUrl,
publicKey,
});

return {
token,
apiBaseUrl,
Expand All @@ -123,5 +166,15 @@ export function getMonobankEnv(): MonobankEnv {
}

export function isMonobankEnabled(): boolean {
return !!resolveMonobankToken();
const token = resolveMonobankToken();
if (!token) return false;

assertMonobankRuntimeConfig({
token,
apiBaseUrl:
nonEmpty(process.env.MONO_API_BASE) ?? 'https://api.monobank.ua',
publicKey: nonEmpty(process.env.MONO_PUBLIC_KEY),
});

return true;
Comment thread
liudmylasovetovs marked this conversation as resolved.
}
Loading
Loading