From 7089813726985f157f9adbb0166a63ed48659551 Mon Sep 17 00:00:00 2001 From: cibss Date: Thu, 10 Sep 2026 12:32:07 +0700 Subject: [PATCH 1/4] feat: add loading states to commerce actions --- apps/orders/src/app/orders/[orderId]/page.tsx | 22 ++++-- .../src/app/payments/[paymentId]/page.tsx | 17 ++-- .../src/app/quotations/[quotationId]/page.tsx | 17 ++-- .../src/app/quotations/new/page.tsx | 15 ++-- package-lock.json | 4 +- packages/ui/package.json | 4 +- packages/ui/src/Button.tsx | 2 +- packages/ui/src/SubmitButton.tsx | 77 +++++++++++++++++++ packages/ui/src/index.ts | 2 + 9 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 packages/ui/src/SubmitButton.tsx diff --git a/apps/orders/src/app/orders/[orderId]/page.tsx b/apps/orders/src/app/orders/[orderId]/page.tsx index 88b3047..c544e15 100644 --- a/apps/orders/src/app/orders/[orderId]/page.tsx +++ b/apps/orders/src/app/orders/[orderId]/page.tsx @@ -3,7 +3,7 @@ import { Panel, PanelHeader, ProgressSteps, - buttonClassName, + SubmitButton, type ProgressStep, } from "@commerce/ui"; import Link from "next/link"; @@ -90,9 +90,9 @@ export default async function OrderDetailPage({
- +
)} @@ -100,7 +100,9 @@ export default async function OrderDetailPage({
- + + Confirm order +
) : null} @@ -108,7 +110,9 @@ export default async function OrderDetailPage({
- + + Start processing +
) : null} @@ -116,7 +120,9 @@ export default async function OrderDetailPage({
- + + Mark shipped +
) : null} @@ -124,7 +130,9 @@ export default async function OrderDetailPage({
- + + Complete order +
) : null} diff --git a/apps/payments/src/app/payments/[paymentId]/page.tsx b/apps/payments/src/app/payments/[paymentId]/page.tsx index 1eb7b48..da801d0 100644 --- a/apps/payments/src/app/payments/[paymentId]/page.tsx +++ b/apps/payments/src/app/payments/[paymentId]/page.tsx @@ -1,5 +1,5 @@ import { createCommerceHref } from "@commerce/platform-ui"; -import { PageHeader, Panel, PanelHeader, buttonClassName } from "@commerce/ui"; +import { PageHeader, Panel, PanelHeader, SubmitButton } from "@commerce/ui"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -59,15 +59,20 @@ export default async function PaymentDetailPage({
- +
- + + Mark as paid +
) : null} @@ -76,9 +81,9 @@ export default async function PaymentDetailPage({
- +
) : null} diff --git a/apps/quotations/src/app/quotations/[quotationId]/page.tsx b/apps/quotations/src/app/quotations/[quotationId]/page.tsx index d53b6e4..f839146 100644 --- a/apps/quotations/src/app/quotations/[quotationId]/page.tsx +++ b/apps/quotations/src/app/quotations/[quotationId]/page.tsx @@ -5,6 +5,7 @@ import { Panel, PanelHeader, ProgressSteps, + SubmitButton, buttonClassName, type ProgressStep, } from "@commerce/ui"; @@ -85,7 +86,9 @@ export default async function QuotationDetailPage({
- + + Send quotation +
) : null} @@ -98,7 +101,9 @@ export default async function QuotationDetailPage({ value={quotation.id} /> - + + Reject +
@@ -108,9 +113,9 @@ export default async function QuotationDetailPage({ value={quotation.id} /> - +
) : null} @@ -119,9 +124,9 @@ export default async function QuotationDetailPage({
- +
) : null} diff --git a/apps/quotations/src/app/quotations/new/page.tsx b/apps/quotations/src/app/quotations/new/page.tsx index d87e4f8..d74d7f6 100644 --- a/apps/quotations/src/app/quotations/new/page.tsx +++ b/apps/quotations/src/app/quotations/new/page.tsx @@ -1,4 +1,10 @@ -import { PageHeader, Panel, PanelHeader, buttonClassName } from "@commerce/ui"; +import { + PageHeader, + Panel, + PanelHeader, + SubmitButton, + buttonClassName, +} from "@commerce/ui"; import Link from "next/link"; import { createQuotationAction } from "@/app/quotations/actions"; @@ -221,12 +227,9 @@ export default async function NewQuotationPage({ - + diff --git a/package-lock.json b/package-lock.json index df4e117..bd27471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10143,12 +10143,14 @@ "version": "0.1.0", "devDependencies": { "@types/react": "^19", + "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.2", "typescript": "^5" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.0", + "react-dom": "^19.2.0" } } } diff --git a/packages/ui/package.json b/packages/ui/package.json index c861714..25e20de 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,10 +12,12 @@ "typecheck": "tsc --noEmit" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.0", + "react-dom": "^19.2.0" }, "devDependencies": { "@types/react": "^19", + "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.2", "typescript": "^5" diff --git a/packages/ui/src/Button.tsx b/packages/ui/src/Button.tsx index 6e7dfe5..6a3b4b6 100644 --- a/packages/ui/src/Button.tsx +++ b/packages/ui/src/Button.tsx @@ -15,7 +15,7 @@ export function buttonClassName( className = "", ) { const base = - "inline-flex items-center justify-center rounded-lg font-semibold transition focus:outline-none focus:ring-2 focus:ring-indigo-500/30 disabled:pointer-events-none disabled:opacity-50"; + "cursor-pointer inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition focus:outline-none focus:ring-2 focus:ring-indigo-500/30 disabled:pointer-events-none disabled:opacity-50"; const variants: Record = { primary: diff --git a/packages/ui/src/SubmitButton.tsx b/packages/ui/src/SubmitButton.tsx new file mode 100644 index 0000000..9f36224 --- /dev/null +++ b/packages/ui/src/SubmitButton.tsx @@ -0,0 +1,77 @@ +"use client"; + +import type { ButtonHTMLAttributes, ReactNode } from "react"; +import { useFormStatus } from "react-dom"; + +import { buttonClassName, type ButtonSize, type ButtonVariant } from "./Button"; + +type SubmitButtonProps = Omit< + ButtonHTMLAttributes, + "children" | "type" +> & { + children: ReactNode; + pendingLabel: ReactNode; + variant?: ButtonVariant; + size?: ButtonSize; +}; + +function LoadingSpinner() { + return ( + + ); +} + +export function SubmitButton({ + children, + pendingLabel, + variant = "primary", + size = "md", + className = "", + disabled, + ...props +}: SubmitButtonProps) { + const { pending } = useFormStatus(); + + const isDisabled = disabled || pending; + + return ( + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d8e2cad..f8c5383 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -18,3 +18,5 @@ export { } from "./ProgressSteps"; export { StatCard } from "./StatCard"; + +export { SubmitButton } from "./SubmitButton"; From ebad4149d4f615747dde741f0e3ad17c6c9c36b3 Mon Sep 17 00:00:00 2001 From: cibss Date: Thu, 10 Sep 2026 17:57:58 +0700 Subject: [PATCH 2/4] feat: integrate payments into order fulfillment --- .../app/api/orders/[orderId]/confirm/route.ts | 4 +- .../api/payments/by-order/[orderId]/route.ts | 24 ++ apps/commerce-api/src/db/seed.ts | 9 +- .../commerce-api/src/db/verify-persistence.ts | 22 +- apps/commerce-api/src/lib/payments.ts | 13 +- .../src/repositories/OrderRepository.ts | 28 ++ .../src/repositories/PaymentRepository.ts | 182 ++++++++-- .../src/services/OrderService.test.ts | 237 ++++++++++++ .../commerce-api/src/services/OrderService.ts | 176 ++++++++- .../src/services/PaymentService.test.ts | 144 ++++++++ .../src/services/PaymentService.ts | 131 +++++++ apps/orders/src/app/orders/[orderId]/page.tsx | 181 ++++++++-- apps/orders/src/app/orders/actions.ts | 21 +- apps/orders/src/lib/commerce-api.ts | 13 +- apps/orders/src/lib/order.ts | 23 ++ apps/payments/src/app/payments/actions.ts | 21 +- .../src/components/PaymentStatusBadge.tsx | 1 + apps/payments/src/lib/payment.ts | 7 +- apps/shell/src/app/page.tsx | 14 +- e2e/order-cancellation.spec.ts | 116 ++++++ e2e/quote-to-cash.spec.ts | 337 ++++++++++++++++++ e2e/quote-to-order.spec.ts | 176 --------- 22 files changed, 1615 insertions(+), 265 deletions(-) create mode 100644 apps/commerce-api/src/app/api/payments/by-order/[orderId]/route.ts create mode 100644 apps/commerce-api/src/services/OrderService.test.ts create mode 100644 apps/commerce-api/src/services/PaymentService.test.ts create mode 100644 e2e/order-cancellation.spec.ts create mode 100644 e2e/quote-to-cash.spec.ts delete mode 100644 e2e/quote-to-order.spec.ts diff --git a/apps/commerce-api/src/app/api/orders/[orderId]/confirm/route.ts b/apps/commerce-api/src/app/api/orders/[orderId]/confirm/route.ts index 5e7eca2..0218248 100644 --- a/apps/commerce-api/src/app/api/orders/[orderId]/confirm/route.ts +++ b/apps/commerce-api/src/app/api/orders/[orderId]/confirm/route.ts @@ -13,10 +13,10 @@ export async function POST(_request: Request, context: RouteContext) { try { const { orderId } = await context.params; - const order = await OrderService.confirm(orderId); + const result = await OrderService.confirm(orderId); return NextResponse.json({ - data: order, + data: result, }); } catch (error) { return createApiErrorResponse(error); diff --git a/apps/commerce-api/src/app/api/payments/by-order/[orderId]/route.ts b/apps/commerce-api/src/app/api/payments/by-order/[orderId]/route.ts new file mode 100644 index 0000000..09cec0c --- /dev/null +++ b/apps/commerce-api/src/app/api/payments/by-order/[orderId]/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; + +import { createApiErrorResponse } from "@/lib/api-error"; +import { PaymentService } from "@/services/PaymentService"; + +type RouteContext = { + params: Promise<{ + orderId: string; + }>; +}; + +export async function GET(_request: Request, context: RouteContext) { + try { + const { orderId } = await context.params; + + const payment = await PaymentService.findByOrderId(orderId); + + return NextResponse.json({ + data: payment, + }); + } catch (error) { + return createApiErrorResponse(error); + } +} diff --git a/apps/commerce-api/src/db/seed.ts b/apps/commerce-api/src/db/seed.ts index d3ecce1..90c0aad 100644 --- a/apps/commerce-api/src/db/seed.ts +++ b/apps/commerce-api/src/db/seed.ts @@ -173,9 +173,9 @@ const orderSeed = [ discount: 8_000_000, total: 160_000_000, currency: "IDR", - status: "PENDING", + status: "CONFIRMED", createdAt: new Date("2026-08-27T03:00:00.000Z"), - updatedAt: new Date("2026-08-27T03:00:00.000Z"), + updatedAt: new Date("2026-08-27T03:15:00.000Z"), }, { id: "ORD-2026-0180", @@ -418,15 +418,10 @@ async function seedDatabase() { console.log(""); console.log(` Customers: ${customerSeed.length}`); - console.log(` Quotations: ${quotationSeed.length}`); - console.log(` Quotation items: ${quotationItemSeed.length}`); - console.log(` Orders: ${orderSeed.length}`); - console.log(` Order items: ${orderItemSeed.length}`); - console.log(` Payments: ${paymentSeed.length}`); } finally { await pool.end(); diff --git a/apps/commerce-api/src/db/verify-persistence.ts b/apps/commerce-api/src/db/verify-persistence.ts index 61244ea..d0aad36 100644 --- a/apps/commerce-api/src/db/verify-persistence.ts +++ b/apps/commerce-api/src/db/verify-persistence.ts @@ -101,9 +101,9 @@ async function verifyPersistence() { throw new Error("Expected ORD-2026-0181 to exist."); } - if (order.status !== "PENDING") { + if (order.status !== "CONFIRMED") { throw new Error( - `Expected ORD-2026-0181 to be PENDING, received ${order.status}.`, + `Expected ORD-2026-0181 to be CONFIRMED, received ${order.status}.`, ); } @@ -113,12 +113,30 @@ async function verifyPersistence() { throw new Error("Expected PAY-2026-0091 to exist."); } + if (payment.orderId !== order.id) { + throw new Error( + `Expected PAY-2026-0091 to belong to ${order.id}, received ${payment.orderId}.`, + ); + } + + if (payment.amount !== order.total) { + throw new Error( + `Expected PAY-2026-0091 amount to equal order total ${order.total}, received ${payment.amount}.`, + ); + } + if (payment.status !== "PENDING") { throw new Error( `Expected PAY-2026-0091 to be PENDING, received ${payment.status}.`, ); } + if (payment.reference !== "INV-2026-0181") { + throw new Error( + `Expected PAY-2026-0091 reference to be INV-2026-0181, received ${payment.reference}.`, + ); + } + if (payment.paidAt !== null) { throw new Error("Expected PAY-2026-0091 paidAt to be null."); } diff --git a/apps/commerce-api/src/lib/payments.ts b/apps/commerce-api/src/lib/payments.ts index 5ceaf04..87439fd 100644 --- a/apps/commerce-api/src/lib/payments.ts +++ b/apps/commerce-api/src/lib/payments.ts @@ -1,4 +1,9 @@ -export type PaymentStatus = "PENDING" | "PAID" | "FAILED" | "REFUNDED"; +export type PaymentStatus = + | "PENDING" + | "PAID" + | "FAILED" + | "REFUNDED" + | "CANCELLED"; export type PaymentMethod = "BANK_TRANSFER" | "VIRTUAL_ACCOUNT" | "CARD"; @@ -18,6 +23,12 @@ export type Payment = { refundedAt: string | null; }; +export type CreatePaymentForOrderInput = { + orderId: string; + amount: number; + currency: Currency; +}; + export class PaymentDomainError extends Error { statusCode: number; diff --git a/apps/commerce-api/src/repositories/OrderRepository.ts b/apps/commerce-api/src/repositories/OrderRepository.ts index af98314..cc2c189 100644 --- a/apps/commerce-api/src/repositories/OrderRepository.ts +++ b/apps/commerce-api/src/repositories/OrderRepository.ts @@ -237,6 +237,33 @@ async function insertInTransaction( } } +async function updateStatusInTransaction( + transaction: DatabaseTransaction, + orderId: string, + expectedStatuses: OrderStatus[], + nextStatus: OrderStatus, + updatedAt: string, +): Promise { + if (expectedStatuses.length === 0) { + return false; + } + + const result = await transaction + .update(orders) + .set({ + status: nextStatus, + updatedAt: new Date(updatedAt), + }) + .where( + and(eq(orders.id, orderId), inArray(orders.status, expectedStatuses)), + ) + .returning({ + id: orders.id, + }); + + return result.length > 0; +} + async function updateStatus( orderId: string, expectedStatuses: OrderStatus[], @@ -272,5 +299,6 @@ export const OrderRepository = { findBySourceQuotationIdInTransaction, allocateIdentityInTransaction, insertInTransaction, + updateStatusInTransaction, updateStatus, }; diff --git a/apps/commerce-api/src/repositories/PaymentRepository.ts b/apps/commerce-api/src/repositories/PaymentRepository.ts index 0e703ec..5a4dbdd 100644 --- a/apps/commerce-api/src/repositories/PaymentRepository.ts +++ b/apps/commerce-api/src/repositories/PaymentRepository.ts @@ -1,7 +1,8 @@ -import { and, desc, eq, inArray } from "drizzle-orm"; +import { and, desc, eq, inArray, like, sql } from "drizzle-orm"; import { getDatabase } from "@/db/client"; import { payments } from "@/db/schema"; +import type { DatabaseTransaction } from "@/db/types"; import type { Currency, Payment, @@ -11,30 +12,61 @@ import type { type PaymentRow = typeof payments.$inferSelect; +export type PaymentIdentity = { + paymentId: string; +}; + function mapPaymentRow(row: PaymentRow): Payment { return { id: row.id, orderId: row.orderId, amount: row.amount, - currency: row.currency as Currency, - method: row.method as PaymentMethod, - status: row.status as PaymentStatus, - reference: row.reference, - createdAt: row.createdAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - paidAt: row.paidAt?.toISOString() ?? null, - refundedAt: row.refundedAt?.toISOString() ?? null, }; } +function getSequenceNumber(id: string | undefined) { + if (!id) { + return 0; + } + + const value = id.split("-").at(-1); + const sequence = Number(value); + + return Number.isInteger(sequence) ? sequence : 0; +} + +function getStatusUpdateValues(nextStatus: PaymentStatus, updatedAt: string) { + const timestamp = new Date(updatedAt); + + const values: { + status: PaymentStatus; + updatedAt: Date; + paidAt?: Date; + refundedAt?: Date; + } = { + status: nextStatus, + updatedAt: timestamp, + }; + + if (nextStatus === "PAID") { + values.paidAt = timestamp; + } + + if (nextStatus === "REFUNDED") { + values.refundedAt = timestamp; + } + + return values; +} + async function list(): Promise { const db = getDatabase(); @@ -62,7 +94,89 @@ async function findById(paymentId: string): Promise { return mapPaymentRow(row); } -async function updateStatus( +async function findByOrderId(orderId: string): Promise { + const db = getDatabase(); + + const [row] = await db + .select() + .from(payments) + .where(eq(payments.orderId, orderId)) + .limit(1); + + if (!row) { + return null; + } + + return mapPaymentRow(row); +} + +async function findByOrderIdInTransaction( + transaction: DatabaseTransaction, + orderId: string, +): Promise { + const [row] = await transaction + .select() + .from(payments) + .where(eq(payments.orderId, orderId)) + .limit(1); + + if (!row) { + return null; + } + + return mapPaymentRow(row); +} + +async function allocateIdentityInTransaction( + transaction: DatabaseTransaction, +): Promise { + await transaction.execute( + sql` + SELECT pg_advisory_xact_lock( + hashtext('commerce:payment-identity') + ) + `, + ); + + const year = new Date().getUTCFullYear(); + + const [latestPayment] = await transaction + .select({ + id: payments.id, + }) + .from(payments) + .where(like(payments.id, `PAY-${year}-%`)) + .orderBy(desc(payments.id)) + .limit(1); + + const paymentSequence = getSequenceNumber(latestPayment?.id) + 1; + + return { + paymentId: `PAY-${year}-${String(paymentSequence).padStart(4, "0")}`, + }; +} + +async function insertInTransaction( + transaction: DatabaseTransaction, + payment: Payment, +) { + await transaction.insert(payments).values({ + id: payment.id, + orderId: payment.orderId, + amount: payment.amount, + currency: payment.currency, + method: payment.method, + status: payment.status, + reference: payment.reference, + createdAt: new Date(payment.createdAt), + updatedAt: new Date(payment.updatedAt), + paidAt: payment.paidAt ? new Date(payment.paidAt) : null, + refundedAt: payment.refundedAt ? new Date(payment.refundedAt) : null, + }); +} + +async function updateStatusInTransaction( + transaction: DatabaseTransaction, paymentId: string, expectedStatuses: PaymentStatus[], nextStatus: PaymentStatus, @@ -72,35 +186,40 @@ async function updateStatus( return false; } - const db = getDatabase(); - - const timestamp = new Date(updatedAt); + const rows = await transaction + .update(payments) + .set(getStatusUpdateValues(nextStatus, updatedAt)) + .where( + and( + eq(payments.id, paymentId), + inArray(payments.status, expectedStatuses), + ), + ) + .returning({ + id: payments.id, + }); - const values: { - status: PaymentStatus; - updatedAt: Date; - paidAt?: Date; - refundedAt?: Date; - } = { - status: nextStatus, - updatedAt: timestamp, - }; + return rows.length > 0; +} - if (nextStatus === "PAID") { - values.paidAt = timestamp; +async function updateStatus( + paymentId: string, + expectedStatuses: PaymentStatus[], + nextStatus: PaymentStatus, + updatedAt: string, +): Promise { + if (expectedStatuses.length === 0) { + return false; } - if (nextStatus === "REFUNDED") { - values.refundedAt = timestamp; - } + const db = getDatabase(); const rows = await db .update(payments) - .set(values) + .set(getStatusUpdateValues(nextStatus, updatedAt)) .where( and( eq(payments.id, paymentId), - inArray(payments.status, expectedStatuses), ), ) @@ -114,5 +233,10 @@ async function updateStatus( export const PaymentRepository = { list, findById, + findByOrderId, + findByOrderIdInTransaction, + allocateIdentityInTransaction, + insertInTransaction, + updateStatusInTransaction, updateStatus, }; diff --git a/apps/commerce-api/src/services/OrderService.test.ts b/apps/commerce-api/src/services/OrderService.test.ts new file mode 100644 index 0000000..7a8b84c --- /dev/null +++ b/apps/commerce-api/src/services/OrderService.test.ts @@ -0,0 +1,237 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { Order } from "@/lib/orders"; +import type { Payment } from "@/lib/payments"; + +const mocks = vi.hoisted(() => ({ + transaction: vi.fn(), + + findOrderById: vi.fn(), + findOrderByIdInTransaction: vi.fn(), + + updateOrderStatus: vi.fn(), + updateOrderStatusInTransaction: vi.fn(), + + findPaymentByOrderId: vi.fn(), + ensurePaymentForOrder: vi.fn(), + handleOrderCancellation: vi.fn(), +})); + +vi.mock("@/db/client", () => ({ + getDatabase: () => ({ + transaction: mocks.transaction, + }), +})); + +vi.mock("@/repositories/OrderRepository", () => ({ + OrderRepository: { + list: vi.fn(), + + findById: mocks.findOrderById, + + findByIdInTransaction: mocks.findOrderByIdInTransaction, + + findBySourceQuotationIdInTransaction: vi.fn(), + + allocateIdentityInTransaction: vi.fn(), + + insertInTransaction: vi.fn(), + + updateStatus: mocks.updateOrderStatus, + + updateStatusInTransaction: mocks.updateOrderStatusInTransaction, + }, +})); + +vi.mock("@/services/PaymentService", () => ({ + PaymentService: { + findByOrderId: mocks.findPaymentByOrderId, + + ensureForOrderInTransaction: mocks.ensurePaymentForOrder, + + handleOrderCancellationInTransaction: mocks.handleOrderCancellation, + }, +})); + +const transaction = {}; + +const confirmedOrder: Order = { + id: "ORD-2026-0182", + sourceQuotationId: "QT-2026-0043", + customerId: "CUST-0192", + customerName: "PT Nusantara Teknologi", + items: [ + { + id: "ORDER-ITEM-005", + sku: "E2E-MONITOR-01", + name: "E2E Test Monitor", + quantity: 2, + unitPrice: 5_000_000, + lineTotal: 10_000_000, + }, + ], + subtotal: 10_000_000, + discount: 1_000_000, + total: 9_000_000, + currency: "IDR", + status: "CONFIRMED", + createdAt: "2026-09-10T00:00:00.000Z", + updatedAt: "2026-09-10T00:01:00.000Z", +}; + +const processingOrder: Order = { + ...confirmedOrder, + status: "PROCESSING", + updatedAt: "2026-09-10T00:02:00.000Z", +}; + +const cancelledOrder: Order = { + ...confirmedOrder, + status: "CANCELLED", + updatedAt: "2026-09-10T00:02:00.000Z", +}; + +const pendingPayment: Payment = { + id: "PAY-2026-0092", + orderId: confirmedOrder.id, + amount: confirmedOrder.total, + currency: "IDR", + method: "BANK_TRANSFER", + status: "PENDING", + reference: "INV-2026-0182", + createdAt: "2026-09-10T00:01:00.000Z", + updatedAt: "2026-09-10T00:01:00.000Z", + paidAt: null, + refundedAt: null, +}; + +const paidPayment: Payment = { + ...pendingPayment, + status: "PAID", + updatedAt: "2026-09-10T00:02:00.000Z", + paidAt: "2026-09-10T00:02:00.000Z", +}; + +const cancelledPayment: Payment = { + ...pendingPayment, + status: "CANCELLED", + updatedAt: "2026-09-10T00:02:00.000Z", +}; + +describe("OrderService payment workflow", () => { + beforeEach(() => { + vi.clearAllMocks(); + + mocks.transaction.mockImplementation( + async (callback: (transaction: object) => Promise) => + callback(transaction), + ); + }); + + it("returns the payment created when an order is confirmed", async () => { + mocks.updateOrderStatusInTransaction.mockResolvedValue(true); + + mocks.findOrderByIdInTransaction.mockResolvedValue(confirmedOrder); + + mocks.ensurePaymentForOrder.mockResolvedValue(pendingPayment); + + const { OrderService } = await import("@/services/OrderService"); + + const result = await OrderService.confirm(confirmedOrder.id); + + expect(result).toEqual({ + order: confirmedOrder, + paymentId: pendingPayment.id, + }); + + expect(mocks.ensurePaymentForOrder).toHaveBeenCalledWith(transaction, { + orderId: confirmedOrder.id, + amount: confirmedOrder.total, + currency: confirmedOrder.currency, + }); + }); + + it("blocks processing while payment is pending", async () => { + mocks.findOrderById.mockResolvedValue(confirmedOrder); + + mocks.findPaymentByOrderId.mockResolvedValue(pendingPayment); + + const { OrderService } = await import("@/services/OrderService"); + + await expect(OrderService.process(confirmedOrder.id)).rejects.toThrow( + `Order ${confirmedOrder.id} cannot start processing until payment is PAID. Current payment status: PENDING.`, + ); + + expect(mocks.updateOrderStatus).not.toHaveBeenCalled(); + }); + + it("allows processing after payment is paid", async () => { + mocks.findOrderById + .mockResolvedValueOnce(confirmedOrder) + .mockResolvedValueOnce(processingOrder); + + mocks.findPaymentByOrderId.mockResolvedValue(paidPayment); + + mocks.updateOrderStatus.mockResolvedValue(true); + + const { OrderService } = await import("@/services/OrderService"); + + const result = await OrderService.process(confirmedOrder.id); + + expect(result).toEqual(processingOrder); + + expect(mocks.updateOrderStatus).toHaveBeenCalledWith( + confirmedOrder.id, + ["CONFIRMED"], + "PROCESSING", + expect.any(String), + ); + }); + + it("cancels the related pending payment with a confirmed order", async () => { + mocks.findOrderByIdInTransaction + .mockResolvedValueOnce(confirmedOrder) + .mockResolvedValueOnce(cancelledOrder); + + mocks.handleOrderCancellation.mockResolvedValue(cancelledPayment); + + mocks.updateOrderStatusInTransaction.mockResolvedValue(true); + + const { OrderService } = await import("@/services/OrderService"); + + const result = await OrderService.cancel(confirmedOrder.id); + + expect(result).toEqual(cancelledOrder); + + expect(mocks.handleOrderCancellation).toHaveBeenCalledWith( + transaction, + confirmedOrder.id, + ); + + expect(mocks.updateOrderStatusInTransaction).toHaveBeenCalledWith( + transaction, + confirmedOrder.id, + ["CONFIRMED"], + "CANCELLED", + expect.any(String), + ); + }); + + it("does not cancel the order when payment cancellation fails", async () => { + mocks.findOrderByIdInTransaction.mockResolvedValue(confirmedOrder); + + mocks.handleOrderCancellation.mockRejectedValue( + new Error( + `Payment ${paidPayment.id} must be refunded before order ${confirmedOrder.id} can be cancelled.`, + ), + ); + + const { OrderService } = await import("@/services/OrderService"); + + await expect(OrderService.cancel(confirmedOrder.id)).rejects.toThrow( + `Payment ${paidPayment.id} must be refunded before order ${confirmedOrder.id} can be cancelled.`, + ); + + expect(mocks.updateOrderStatusInTransaction).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/commerce-api/src/services/OrderService.ts b/apps/commerce-api/src/services/OrderService.ts index 1e48d79..4227062 100644 --- a/apps/commerce-api/src/services/OrderService.ts +++ b/apps/commerce-api/src/services/OrderService.ts @@ -9,6 +9,12 @@ import { type OrderStatus, } from "@/lib/orders"; import { OrderRepository } from "@/repositories/OrderRepository"; +import { PaymentService } from "@/services/PaymentService"; + +export type ConfirmOrderResult = { + order: Order; + paymentId: string; +}; async function list(): Promise { return OrderRepository.list(); @@ -47,7 +53,6 @@ export async function createOrderFromQuotationInTransaction( const items: OrderItem[] = input.items.map((item, index) => ({ id: identity.itemIds[index], - sku: item.sku, name: item.name, quantity: item.quantity, @@ -57,25 +62,15 @@ export async function createOrderFromQuotationInTransaction( const order: Order = { id: identity.orderId, - sourceQuotationId: input.sourceQuotationId, - customerId: input.customerId, - customerName: input.customerName, - items, - subtotal: input.subtotal, - discount: input.discount, - total: input.total, - currency: input.currency, - status: "PENDING", - createdAt: timestamp, updatedAt: timestamp, }; @@ -127,11 +122,85 @@ async function transition( return getById(orderId); } -async function confirm(orderId: string) { - return transition(orderId, ["PENDING"], "CONFIRMED"); +async function confirm(orderId: string): Promise { + const db = getDatabase(); + + return db.transaction(async (transaction) => { + const updated = await OrderRepository.updateStatusInTransaction( + transaction, + orderId, + ["PENDING"], + "CONFIRMED", + new Date().toISOString(), + ); + + if (!updated) { + const current = await OrderRepository.findByIdInTransaction( + transaction, + orderId, + ); + + if (!current) { + throw new OrderDomainError(`Order ${orderId} was not found.`, 404); + } + + assertOrderTransition(orderId, current.status, ["PENDING"], "CONFIRMED"); + + throw new OrderDomainError( + `Order ${orderId} could not be confirmed.`, + 409, + ); + } + + const confirmedOrder = await OrderRepository.findByIdInTransaction( + transaction, + orderId, + ); + + if (!confirmedOrder) { + throw new OrderDomainError( + `Order ${orderId} could not be loaded after confirmation.`, + 409, + ); + } + + const payment = await PaymentService.ensureForOrderInTransaction( + transaction, + { + orderId: confirmedOrder.id, + amount: confirmedOrder.total, + currency: confirmedOrder.currency, + }, + ); + + return { + order: confirmedOrder, + paymentId: payment.id, + }; + }); } -async function process(orderId: string) { +async function process(orderId: string): Promise { + const order = await getById(orderId); + + assertOrderTransition(orderId, order.status, ["CONFIRMED"], "PROCESSING"); + + const payment = await PaymentService.findByOrderId(orderId); + + if (!payment) { + throw new OrderDomainError( + `Order ${orderId} cannot start processing because its payment record was not found.`, + 409, + ); + } + + if (payment.status !== "PAID") { + throw new OrderDomainError( + `Order ${orderId} cannot start processing until payment is PAID. Current payment status: ${payment.status}.`, + 409, + ); + } + return transition(orderId, ["CONFIRMED"], "PROCESSING"); } @@ -143,8 +212,83 @@ async function complete(orderId: string) { return transition(orderId, ["SHIPPED"], "COMPLETED"); } -async function cancel(orderId: string) { - return transition(orderId, ["PENDING", "CONFIRMED"], "CANCELLED"); +async function cancel(orderId: string): Promise { + const db = getDatabase(); + + return db.transaction(async (transaction) => { + const currentOrder = await OrderRepository.findByIdInTransaction( + transaction, + orderId, + ); + + if (!currentOrder) { + throw new OrderDomainError(`Order ${orderId} was not found.`, 404); + } + + assertOrderTransition( + orderId, + currentOrder.status, + ["PENDING", "CONFIRMED"], + "CANCELLED", + ); + + const payment = await PaymentService.handleOrderCancellationInTransaction( + transaction, + orderId, + ); + + if (currentOrder.status === "CONFIRMED" && !payment) { + throw new OrderDomainError( + `Confirmed order ${orderId} cannot be cancelled because its payment record was not found.`, + 409, + ); + } + + const updated = await OrderRepository.updateStatusInTransaction( + transaction, + orderId, + [currentOrder.status], + "CANCELLED", + new Date().toISOString(), + ); + + if (!updated) { + const latestOrder = await OrderRepository.findByIdInTransaction( + transaction, + orderId, + ); + + if (!latestOrder) { + throw new OrderDomainError(`Order ${orderId} was not found.`, 404); + } + + assertOrderTransition( + orderId, + latestOrder.status, + ["PENDING", "CONFIRMED"], + "CANCELLED", + ); + + throw new OrderDomainError( + `Order ${orderId} could not be cancelled.`, + 409, + ); + } + + const cancelledOrder = await OrderRepository.findByIdInTransaction( + transaction, + orderId, + ); + + if (!cancelledOrder) { + throw new OrderDomainError( + `Order ${orderId} could not be loaded after cancellation.`, + 409, + ); + } + + return cancelledOrder; + }); } export const OrderService = { diff --git a/apps/commerce-api/src/services/PaymentService.test.ts b/apps/commerce-api/src/services/PaymentService.test.ts new file mode 100644 index 0000000..b51ea8f --- /dev/null +++ b/apps/commerce-api/src/services/PaymentService.test.ts @@ -0,0 +1,144 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { DatabaseTransaction } from "@/db/types"; +import type { Payment } from "@/lib/payments"; + +const mocks = vi.hoisted(() => ({ + findByOrderIdInTransaction: vi.fn(), + updateStatusInTransaction: vi.fn(), +})); + +vi.mock("@/repositories/PaymentRepository", () => ({ + PaymentRepository: { + list: vi.fn(), + findById: vi.fn(), + findByOrderId: vi.fn(), + + findByOrderIdInTransaction: mocks.findByOrderIdInTransaction, + + allocateIdentityInTransaction: vi.fn(), + insertInTransaction: vi.fn(), + + updateStatusInTransaction: mocks.updateStatusInTransaction, + + updateStatus: vi.fn(), + }, +})); + +const transaction = {} as DatabaseTransaction; + +const pendingPayment: Payment = { + id: "PAY-2026-0092", + orderId: "ORD-2026-0182", + amount: 9_000_000, + currency: "IDR", + method: "BANK_TRANSFER", + status: "PENDING", + reference: "INV-2026-0182", + createdAt: "2026-09-10T00:00:00.000Z", + updatedAt: "2026-09-10T00:00:00.000Z", + paidAt: null, + refundedAt: null, +}; + +const cancelledPayment: Payment = { + ...pendingPayment, + status: "CANCELLED", + updatedAt: "2026-09-10T00:01:00.000Z", +}; + +const paidPayment: Payment = { + ...pendingPayment, + status: "PAID", + updatedAt: "2026-09-10T00:01:00.000Z", + paidAt: "2026-09-10T00:01:00.000Z", +}; + +describe("PaymentService order cancellation", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("cancels a pending payment with its order", async () => { + mocks.findByOrderIdInTransaction + .mockResolvedValueOnce(pendingPayment) + .mockResolvedValueOnce(cancelledPayment); + + mocks.updateStatusInTransaction.mockResolvedValue(true); + + const { PaymentService } = await import("@/services/PaymentService"); + + const result = await PaymentService.handleOrderCancellationInTransaction( + transaction, + pendingPayment.orderId, + ); + + expect(result).toEqual(cancelledPayment); + + expect(mocks.updateStatusInTransaction).toHaveBeenCalledWith( + transaction, + pendingPayment.id, + ["PENDING"], + "CANCELLED", + expect.any(String), + ); + }); + + it("requires a paid payment to be refunded before cancellation", async () => { + mocks.findByOrderIdInTransaction.mockResolvedValue(paidPayment); + + const { PaymentService } = await import("@/services/PaymentService"); + + await expect( + PaymentService.handleOrderCancellationInTransaction( + transaction, + paidPayment.orderId, + ), + ).rejects.toThrow( + `Payment ${paidPayment.id} must be refunded before order ${paidPayment.orderId} can be cancelled.`, + ); + + expect(mocks.updateStatusInTransaction).not.toHaveBeenCalled(); + }); + + it("preserves a failed payment when its order is cancelled", async () => { + const failedPayment: Payment = { + ...pendingPayment, + status: "FAILED", + }; + + mocks.findByOrderIdInTransaction.mockResolvedValue(failedPayment); + + const { PaymentService } = await import("@/services/PaymentService"); + + const result = await PaymentService.handleOrderCancellationInTransaction( + transaction, + failedPayment.orderId, + ); + + expect(result).toEqual(failedPayment); + + expect(mocks.updateStatusInTransaction).not.toHaveBeenCalled(); + }); + + it("preserves a refunded payment when its order is cancelled", async () => { + const refundedPayment: Payment = { + ...paidPayment, + status: "REFUNDED", + refundedAt: "2026-09-10T00:02:00.000Z", + }; + + mocks.findByOrderIdInTransaction.mockResolvedValue(refundedPayment); + + const { PaymentService } = await import("@/services/PaymentService"); + + const result = await PaymentService.handleOrderCancellationInTransaction( + transaction, + refundedPayment.orderId, + ); + + expect(result).toEqual(refundedPayment); + + expect(mocks.updateStatusInTransaction).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/commerce-api/src/services/PaymentService.ts b/apps/commerce-api/src/services/PaymentService.ts index 62d7bb4..c8982c6 100644 --- a/apps/commerce-api/src/services/PaymentService.ts +++ b/apps/commerce-api/src/services/PaymentService.ts @@ -1,5 +1,7 @@ +import type { DatabaseTransaction } from "@/db/types"; import { assertPaymentTransition, + type CreatePaymentForOrderInput, type Payment, PaymentDomainError, type PaymentStatus, @@ -20,6 +22,132 @@ async function getById(paymentId: string): Promise { return payment; } +async function findByOrderId(orderId: string): Promise { + return PaymentRepository.findByOrderId(orderId); +} + +export async function ensurePaymentForOrderInTransaction( + transaction: DatabaseTransaction, + input: CreatePaymentForOrderInput, +): Promise { + const existingPayment = await PaymentRepository.findByOrderIdInTransaction( + transaction, + input.orderId, + ); + + if (existingPayment) { + return existingPayment; + } + + const identity = + await PaymentRepository.allocateIdentityInTransaction(transaction); + + const timestamp = new Date().toISOString(); + + const payment: Payment = { + id: identity.paymentId, + orderId: input.orderId, + amount: input.amount, + currency: input.currency, + method: "BANK_TRANSFER", + status: "PENDING", + reference: input.orderId.replace(/^ORD-/, "INV-"), + createdAt: timestamp, + updatedAt: timestamp, + paidAt: null, + refundedAt: null, + }; + + await PaymentRepository.insertInTransaction(transaction, payment); + + return payment; +} + +async function handleOrderCancellationInTransaction( + transaction: DatabaseTransaction, + orderId: string, +): Promise { + const payment = await PaymentRepository.findByOrderIdInTransaction( + transaction, + orderId, + ); + + if (!payment) { + return null; + } + + if (payment.status === "PAID") { + throw new PaymentDomainError( + `Payment ${payment.id} must be refunded before order ${orderId} can be cancelled.`, + 409, + ); + } + + if ( + payment.status === "FAILED" || + payment.status === "REFUNDED" || + payment.status === "CANCELLED" + ) { + return payment; + } + + const updated = await PaymentRepository.updateStatusInTransaction( + transaction, + payment.id, + ["PENDING"], + "CANCELLED", + new Date().toISOString(), + ); + + if (!updated) { + const currentPayment = await PaymentRepository.findByOrderIdInTransaction( + transaction, + orderId, + ); + + if (!currentPayment) { + throw new PaymentDomainError( + `Payment for order ${orderId} was not found.`, + 404, + ); + } + + if (currentPayment.status === "PAID") { + throw new PaymentDomainError( + `Payment ${currentPayment.id} must be refunded before order ${orderId} can be cancelled.`, + 409, + ); + } + + if ( + currentPayment.status === "FAILED" || + currentPayment.status === "REFUNDED" || + currentPayment.status === "CANCELLED" + ) { + return currentPayment; + } + + throw new PaymentDomainError( + `Payment ${currentPayment.id} could not be cancelled.`, + 409, + ); + } + + const cancelledPayment = await PaymentRepository.findByOrderIdInTransaction( + transaction, + orderId, + ); + + if (!cancelledPayment) { + throw new PaymentDomainError( + `Payment for order ${orderId} could not be loaded after cancellation.`, + 409, + ); + } + + return cancelledPayment; +} + async function transition( paymentId: string, expectedStatuses: PaymentStatus[], @@ -70,6 +198,9 @@ async function refund(paymentId: string) { export const PaymentService = { list, getById, + findByOrderId, + ensureForOrderInTransaction: ensurePaymentForOrderInTransaction, + handleOrderCancellationInTransaction, markPaid, markFailed, refund, diff --git a/apps/orders/src/app/orders/[orderId]/page.tsx b/apps/orders/src/app/orders/[orderId]/page.tsx index c544e15..998c855 100644 --- a/apps/orders/src/app/orders/[orderId]/page.tsx +++ b/apps/orders/src/app/orders/[orderId]/page.tsx @@ -1,9 +1,12 @@ +import { createCommerceHref } from "@commerce/platform-ui"; import { + Badge, PageHeader, Panel, PanelHeader, ProgressSteps, - SubmitButton, + buttonClassName, + type BadgeVariant, type ProgressStep, } from "@commerce/ui"; import Link from "next/link"; @@ -17,8 +20,12 @@ import { shipOrderAction, } from "@/app/orders/actions"; import { OrderStatusBadge } from "@/components/OrderStatusBadge"; -import { getOrder } from "@/lib/commerce-api"; -import { formatCurrency, formatDateTime } from "@/lib/order"; +import { getOrder, getOrderPayment } from "@/lib/commerce-api"; +import { + formatCurrency, + formatDateTime, + type OrderPaymentStatus, +} from "@/lib/order"; export const dynamic = "force-dynamic"; @@ -39,15 +46,45 @@ const lifecycle = [ "COMPLETED", ] as const; +const paymentStatusVariants: Record = { + PENDING: "warning", + PAID: "success", + FAILED: "danger", + REFUNDED: "accent", + CANCELLED: "neutral", +}; + +function getPaymentMessage(status: OrderPaymentStatus) { + if (status === "PAID") { + return "Payment has cleared. This order is ready for fulfillment. Refund the payment first if the confirmed order needs to be cancelled."; + } + + if (status === "FAILED") { + return "Payment failed. Fulfillment remains locked, but the order can still be cancelled."; + } + + if (status === "REFUNDED") { + return "Payment has been refunded. Fulfillment is locked and the confirmed order can now be cancelled."; + } + + if (status === "CANCELLED") { + return "This payment was cancelled because the related order was cancelled."; + } + + return "Waiting for payment clearance. Fulfillment remains locked. Cancelling the order will also cancel this pending payment."; +} + export default async function OrderDetailPage({ params, searchParams, }: OrderDetailPageProps) { const { orderId } = await params; - const { error } = await searchParams; - const order = await getOrder(orderId); + const [order, payment] = await Promise.all([ + getOrder(orderId), + getOrderPayment(orderId), + ]); if (!order) { notFound(); @@ -69,6 +106,20 @@ export default async function OrderDetailPage({ : "upcoming", })); + const paymentCleared = payment?.status === "PAID"; + + const canStartProcessing = order.status === "CONFIRMED" && paymentCleared; + + const canCancelOrder = + order.status === "PENDING" || + (order.status === "CONFIRMED" && + payment !== null && + payment.status !== "PAID"); + + const processingBlockedMessage = payment + ? `Payment ${payment.id} is ${payment.status}. Payment must be PAID before fulfillment can begin.` + : "A payment must exist and be PAID before fulfillment can begin."; + return ( <> - + )} @@ -100,9 +154,7 @@ export default async function OrderDetailPage({
- - Confirm order - +
) : null} @@ -110,9 +162,17 @@ export default async function OrderDetailPage({
- + ) : null} @@ -120,9 +180,7 @@ export default async function OrderDetailPage({
- - Mark shipped - +
) : null} @@ -130,9 +188,7 @@ export default async function OrderDetailPage({
- - Complete order - +
) : null} @@ -150,7 +206,7 @@ export default async function OrderDetailPage({
@@ -162,7 +218,9 @@ export default async function OrderDetailPage({
@@ -235,6 +293,89 @@ export default async function OrderDetailPage({
+ +
+

+ Payment clearance +

+ + {payment ? ( + <> +
+

+ {payment.id} +

+ + + {payment.status} + +
+ +
+
+
Amount
+ +
+ {formatCurrency(payment.amount, payment.currency)} +
+
+ +
+
Method
+ +
+ {payment.method.replaceAll("_", " ")} +
+
+ +
+
Reference
+ +
+ {payment.reference} +
+
+
+ +

+ {getPaymentMessage(payment.status)} +

+ + + View payment → + + + ) : ( + <> +
+ NOT CREATED +
+ +

+ {order.status === "PENDING" + ? "A payment record will be created automatically when this order is confirmed." + : "No payment record was found for this order. Fulfillment cannot begin until payment clearance exists."} +

+ + )} +
+
+

diff --git a/apps/orders/src/app/orders/actions.ts b/apps/orders/src/app/orders/actions.ts index c5a59f0..7ad538d 100644 --- a/apps/orders/src/app/orders/actions.ts +++ b/apps/orders/src/app/orders/actions.ts @@ -42,6 +42,10 @@ function getOrderUrl(orderId: string) { return `${getCommercePlatformUrl()}/orders/${orderId}`; } +function getPaymentUrl(paymentId: string) { + return `${getCommercePlatformUrl()}/payments/${paymentId}`; +} + async function runOrderTransition( orderId: string, transition: () => Promise, @@ -63,7 +67,22 @@ async function runOrderTransition( export async function confirmOrderAction(formData: FormData) { const orderId = getString(formData, "orderId"); - await runOrderTransition(orderId, () => confirmOrder(orderId)); + let paymentId: string; + + try { + const result = await confirmOrder(orderId); + + paymentId = result.paymentId; + } catch (error) { + const message = encodeURIComponent(getErrorMessage(error)); + + redirect(`${getOrderUrl(orderId)}?error=${message}`); + } + + revalidatePath("/orders"); + revalidatePath(`/orders/${orderId}`); + + redirect(getPaymentUrl(paymentId)); } export async function processOrderAction(formData: FormData) { diff --git a/apps/orders/src/lib/commerce-api.ts b/apps/orders/src/lib/commerce-api.ts index 85cf461..1a70fad 100644 --- a/apps/orders/src/lib/commerce-api.ts +++ b/apps/orders/src/lib/commerce-api.ts @@ -1,4 +1,4 @@ -import type { Order } from "@/lib/order"; +import type { Order, OrderPayment } from "@/lib/order"; type ApiSuccess = { data: T; @@ -10,6 +10,11 @@ type ApiFailure = { }; }; +export type ConfirmOrderResult = { + order: Order; + paymentId: string; +}; + export class CommerceApiError extends Error { statusCode: number; @@ -72,8 +77,12 @@ export async function getOrder(orderId: string) { } } +export function getOrderPayment(orderId: string) { + return request(`/api/payments/by-order/${orderId}`); +} + export function confirmOrder(orderId: string) { - return request(`/api/orders/${orderId}/confirm`, { + return request(`/api/orders/${orderId}/confirm`, { method: "POST", }); } diff --git a/apps/orders/src/lib/order.ts b/apps/orders/src/lib/order.ts index 34aba70..548d260 100644 --- a/apps/orders/src/lib/order.ts +++ b/apps/orders/src/lib/order.ts @@ -32,6 +32,29 @@ export type Order = { updatedAt: string; }; +export type OrderPaymentStatus = + | "PENDING" + | "PAID" + | "FAILED" + | "REFUNDED" + | "CANCELLED"; + +export type OrderPaymentMethod = "BANK_TRANSFER" | "VIRTUAL_ACCOUNT" | "CARD"; + +export type OrderPayment = { + id: string; + orderId: string; + amount: number; + currency: Currency; + method: OrderPaymentMethod; + status: OrderPaymentStatus; + reference: string; + createdAt: string; + updatedAt: string; + paidAt: string | null; + refundedAt: string | null; +}; + export function formatCurrency(amount: number, currency: Currency) { return new Intl.NumberFormat("id-ID", { style: "currency", diff --git a/apps/payments/src/app/payments/actions.ts b/apps/payments/src/app/payments/actions.ts index e301289..0f35396 100644 --- a/apps/payments/src/app/payments/actions.ts +++ b/apps/payments/src/app/payments/actions.ts @@ -40,6 +40,10 @@ function getPaymentUrl(paymentId: string) { return `${getCommercePlatformUrl()}/payments/${paymentId}`; } +function getOrderUrl(orderId: string) { + return `${getCommercePlatformUrl()}/orders/${orderId}`; +} + async function runPaymentTransition( paymentId: string, transition: () => Promise, @@ -61,7 +65,22 @@ async function runPaymentTransition( export async function markPaymentPaidAction(formData: FormData) { const paymentId = getString(formData, "paymentId"); - await runPaymentTransition(paymentId, () => markPaymentPaid(paymentId)); + let orderId: string; + + try { + const payment = await markPaymentPaid(paymentId); + + orderId = payment.orderId; + } catch (error) { + const message = encodeURIComponent(getErrorMessage(error)); + + redirect(`${getPaymentUrl(paymentId)}?error=${message}`); + } + + revalidatePath("/payments"); + revalidatePath(`/payments/${paymentId}`); + + redirect(getOrderUrl(orderId)); } export async function markPaymentFailedAction(formData: FormData) { diff --git a/apps/payments/src/components/PaymentStatusBadge.tsx b/apps/payments/src/components/PaymentStatusBadge.tsx index 7a6f46d..a54269a 100644 --- a/apps/payments/src/components/PaymentStatusBadge.tsx +++ b/apps/payments/src/components/PaymentStatusBadge.tsx @@ -11,6 +11,7 @@ const statusVariants: Record = { PAID: "success", FAILED: "danger", REFUNDED: "accent", + CANCELLED: "neutral", }; export function PaymentStatusBadge({ status }: PaymentStatusBadgeProps) { diff --git a/apps/payments/src/lib/payment.ts b/apps/payments/src/lib/payment.ts index 9f11064..3b8ba49 100644 --- a/apps/payments/src/lib/payment.ts +++ b/apps/payments/src/lib/payment.ts @@ -1,4 +1,9 @@ -export type PaymentStatus = "PENDING" | "PAID" | "FAILED" | "REFUNDED"; +export type PaymentStatus = + | "PENDING" + | "PAID" + | "FAILED" + | "REFUNDED" + | "CANCELLED"; export type PaymentMethod = "BANK_TRANSFER" | "VIRTUAL_ACCOUNT" | "CARD"; diff --git a/apps/shell/src/app/page.tsx b/apps/shell/src/app/page.tsx index a62b62b..100a264 100644 --- a/apps/shell/src/app/page.tsx +++ b/apps/shell/src/app/page.tsx @@ -15,7 +15,7 @@ const domains = [ { title: "Orders", description: - "Manage order confirmation, fulfillment, shipping, and completion after commercial handover.", + "Manage confirmed customer orders through payment clearance, fulfillment, shipping, and completion.", href: "/orders", ownership: "Order Operations", accent: "from-blue-500 to-cyan-500", @@ -35,7 +35,7 @@ const domains = [ { title: "Payments", description: - "Monitor payment settlement, failed transactions, completed payments, and refunds.", + "Track payment settlement for confirmed orders, including pending, paid, failed, and refunded transactions.", href: "/payments", ownership: "Finance Operations", accent: "from-emerald-500 to-teal-500", @@ -48,8 +48,8 @@ const workflow = [ "Quotation", "Approval", "Order", - "Fulfillment", "Payment", + "Fulfillment", "Completed", ]; @@ -59,7 +59,7 @@ export default function Home() {

Microfrontend architecture @@ -94,8 +94,8 @@ export default function Home() {
@@ -103,7 +103,7 @@ export default function Home() { Operational} /> diff --git a/e2e/order-cancellation.spec.ts b/e2e/order-cancellation.spec.ts new file mode 100644 index 0000000..8e95393 --- /dev/null +++ b/e2e/order-cancellation.spec.ts @@ -0,0 +1,116 @@ +import { expect, test } from "@playwright/test"; + +const COMMERCE_BASE_PATH = "/work/commerce-operations"; +const COMMERCE_API_ORIGIN = "http://localhost:3003"; + +test.describe("order cancellation workflow", () => { + test("cancels a pending payment when its confirmed order is cancelled", async ({ + page, + request, + }) => { + const orderId = "ORD-2026-0181"; + const paymentId = "PAY-2026-0091"; + + await page.goto(`${COMMERCE_BASE_PATH}/orders/${orderId}`); + + await expect( + page.getByRole("heading", { + name: orderId, + }), + ).toBeVisible(); + + await expect( + page.getByText(paymentId, { + exact: true, + }), + ).toBeVisible(); + + await expect( + page + .getByText("PENDING", { + exact: true, + }) + .first(), + ).toBeVisible(); + + await expect( + page.getByRole("button", { + name: "Start processing", + }), + ).toBeDisabled(); + + const cancelButton = page.getByRole("button", { + name: "Cancel order", + }); + + await expect(cancelButton).toBeEnabled(); + + await cancelButton.click(); + + await expect( + page + .getByText("CANCELLED", { + exact: true, + }) + .first(), + ).toBeVisible(); + + await expect( + page.getByRole("button", { + name: "Start processing", + }), + ).not.toBeVisible(); + + await Promise.all([ + page.waitForURL( + new RegExp(`/work/commerce-operations/payments/${paymentId}$`), + { + timeout: 20_000, + waitUntil: "commit", + }, + ), + + page + .getByRole("link", { + name: /View payment/i, + }) + .click(), + ]); + + await expect( + page.getByRole("heading", { + name: paymentId, + }), + ).toBeVisible(); + + await expect( + page + .getByText("CANCELLED", { + exact: true, + }) + .first(), + ).toBeVisible(); + + await expect( + page.getByRole("button", { + name: "Mark as paid", + }), + ).not.toBeVisible(); + + const blockedPaymentResponse = await request.post( + `${COMMERCE_API_ORIGIN}/api/payments/${paymentId}/pay`, + ); + + expect(blockedPaymentResponse.status()).toBe(409); + + const blockedPaymentBody = (await blockedPaymentResponse.json()) as { + error?: { + message?: string; + }; + }; + + expect(blockedPaymentBody.error?.message).toContain( + "cannot transition from CANCELLED to PAID", + ); + }); +}); diff --git a/e2e/quote-to-cash.spec.ts b/e2e/quote-to-cash.spec.ts new file mode 100644 index 0000000..6d01012 --- /dev/null +++ b/e2e/quote-to-cash.spec.ts @@ -0,0 +1,337 @@ +import { expect, test } from "@playwright/test"; + +const COMMERCE_BASE_PATH = "/work/commerce-operations"; +const COMMERCE_API_ORIGIN = "http://localhost:3003"; + +test.describe("quote-to-cash workflow", () => { + test("moves a quotation through payment clearance and fulfillment across zones", async ({ + page, + request, + }) => { + test.setTimeout(90_000); + + await page.goto(`${COMMERCE_BASE_PATH}/quotations`); + + await expect( + page.getByRole("heading", { + name: "Quotations", + }), + ).toBeVisible(); + + await page + .getByRole("link", { + name: /Create quotation/i, + }) + .click(); + + await expect( + page.getByRole("heading", { + name: "Create quotation", + }), + ).toBeVisible(); + + await page.getByLabel("Customer account").selectOption("CUST-0192"); + await page.getByLabel("SKU").fill("E2E-MONITOR-01"); + await page.getByLabel("Item name").fill("E2E Test Monitor"); + await page.getByLabel("Quantity").fill("2"); + await page.getByLabel("Unit price").fill("5000000"); + await page.getByLabel("Discount").fill("1000000"); + + await page + .getByRole("button", { + name: "Create draft quotation", + }) + .click(); + + await expect(page).toHaveURL( + /\/work\/commerce-operations\/quotations\/QT-\d{4}-\d{4}$/, + ); + + const quotationId = page.url().split("/").pop(); + + expect(quotationId).toBeTruthy(); + + if (!quotationId) { + throw new Error( + "Quotation ID was not found after creating the quotation.", + ); + } + + await expect( + page.getByRole("button", { + name: "Send quotation", + }), + ).toBeVisible(); + + await page + .getByRole("button", { + name: "Send quotation", + }) + .click(); + + await expect( + page.getByRole("button", { + name: "Accept quotation", + }), + ).toBeVisible(); + + await page + .getByRole("button", { + name: "Accept quotation", + }) + .click(); + + const convertButton = page.getByRole("button", { + name: /Convert to order/i, + }); + + await expect(convertButton).toBeVisible(); + + await Promise.all([ + page.waitForURL(/\/work\/commerce-operations\/orders\/ORD-\d{4}-\d{4}$/, { + timeout: 20_000, + waitUntil: "commit", + }), + + convertButton.click(), + ]); + + const orderId = page.url().split("/").pop(); + + expect(orderId).toBeTruthy(); + + if (!orderId) { + throw new Error("Order ID was not found after converting the quotation."); + } + + expect(orderId).toMatch(/^ORD-\d{4}-\d{4}$/); + + await expect( + page.getByRole("heading", { + name: orderId, + }), + ).toBeVisible(); + + await expect( + page + .getByText(quotationId, { + exact: true, + }) + .first(), + ).toBeVisible(); + + // A converted order starts as PENDING and must not have a payment yet. + await expect( + page.getByText("NOT CREATED", { + exact: true, + }), + ).toBeVisible(); + + const confirmButton = page.getByRole("button", { + name: "Confirm order", + }); + + await Promise.all([ + page.waitForURL( + /\/work\/commerce-operations\/payments\/PAY-\d{4}-\d{4}$/, + { + timeout: 20_000, + waitUntil: "commit", + }, + ), + + confirmButton.click(), + ]); + + const paymentId = page.url().split("/").pop(); + + expect(paymentId).toBeTruthy(); + + if (!paymentId) { + throw new Error("Payment ID was not found after confirming the order."); + } + + expect(paymentId).toMatch(/^PAY-\d{4}-\d{4}$/); + + await expect( + page.getByRole("heading", { + name: paymentId, + }), + ).toBeVisible(); + + await expect( + page + .getByText("PENDING", { + exact: true, + }) + .first(), + ).toBeVisible(); + + // Backend must reject fulfillment while payment is still pending. + const blockedProcessingResponse = await request.post( + `${COMMERCE_API_ORIGIN}/api/orders/${orderId}/process`, + ); + + expect(blockedProcessingResponse.status()).toBe(409); + + const blockedProcessingBody = (await blockedProcessingResponse.json()) as { + error?: { + message?: string; + }; + }; + + expect(blockedProcessingBody.error?.message).toContain( + "cannot start processing until payment is PAID", + ); + + // Return to the order before paying to verify the UI gate. + await Promise.all([ + page.waitForURL( + new RegExp(`/work/commerce-operations/orders/${orderId}$`), + { + timeout: 20_000, + waitUntil: "commit", + }, + ), + + page + .getByRole("link", { + name: /View order/i, + }) + .click(), + ]); + + await expect( + page.getByText(paymentId, { + exact: true, + }), + ).toBeVisible(); + + await expect( + page + .getByText("PENDING", { + exact: true, + }) + .first(), + ).toBeVisible(); + + await expect( + page.getByRole("button", { + name: "Start processing", + }), + ).toBeDisabled(); + + // Go back to the payment and clear it. + await Promise.all([ + page.waitForURL( + new RegExp(`/work/commerce-operations/payments/${paymentId}$`), + { + timeout: 20_000, + waitUntil: "commit", + }, + ), + + page + .getByRole("link", { + name: /View payment/i, + }) + .click(), + ]); + + const markPaidButton = page.getByRole("button", { + name: "Mark as paid", + }); + + await expect(markPaidButton).toBeVisible(); + + await Promise.all([ + page.waitForURL( + new RegExp(`/work/commerce-operations/orders/${orderId}$`), + { + timeout: 20_000, + waitUntil: "commit", + }, + ), + + markPaidButton.click(), + ]); + + await expect( + page.getByText(paymentId, { + exact: true, + }), + ).toBeVisible(); + + await expect( + page + .getByText("PAID", { + exact: true, + }) + .first(), + ).toBeVisible(); + + const startProcessingButton = page.getByRole("button", { + name: "Start processing", + }); + + await expect(startProcessingButton).toBeEnabled(); + + // A PAID order cannot be cancelled before its payment is refunded. + await expect( + page.getByRole("button", { + name: "Cancel order", + }), + ).toBeDisabled(); + + const blockedCancellationResponse = await request.post( + `${COMMERCE_API_ORIGIN}/api/orders/${orderId}/cancel`, + ); + + expect(blockedCancellationResponse.status()).toBe(409); + + const blockedCancellationBody = + (await blockedCancellationResponse.json()) as { + error?: { + message?: string; + }; + }; + + expect(blockedCancellationBody.error?.message).toContain( + "must be refunded before order", + ); + + await startProcessingButton.click(); + + await expect( + page.getByRole("button", { + name: "Mark shipped", + }), + ).toBeVisible(); + + await page + .getByRole("button", { + name: "Mark shipped", + }) + .click(); + + await expect( + page.getByRole("button", { + name: "Complete order", + }), + ).toBeVisible(); + + await page + .getByRole("button", { + name: "Complete order", + }) + .click(); + + await expect( + page + .getByText("COMPLETED", { + exact: true, + }) + .first(), + ).toBeVisible(); + }); +}); diff --git a/e2e/quote-to-order.spec.ts b/e2e/quote-to-order.spec.ts deleted file mode 100644 index 5e37b77..0000000 --- a/e2e/quote-to-order.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { expect, test } from "@playwright/test"; - -const COMMERCE_BASE_PATH = "/work/commerce-operations"; - -test.describe("quote-to-order workflow", () => { - test("creates, approves, converts, and fulfills an order across microfrontends", async ({ - page, - }) => { - test.setTimeout(60_000); - - await page.goto(`${COMMERCE_BASE_PATH}/quotations`); - - await expect( - page.getByRole("heading", { - name: "Quotations", - }), - ).toBeVisible(); - - await page - .getByRole("link", { - name: /Create quotation/i, - }) - .click(); - - await expect( - page.getByRole("heading", { - name: "Create quotation", - }), - ).toBeVisible(); - - await page.getByLabel("Customer account").selectOption("CUST-0192"); - - await page.getByLabel("SKU").fill("E2E-MONITOR-01"); - - await page.getByLabel("Item name").fill("E2E Test Monitor"); - - await page.getByLabel("Quantity").fill("2"); - - await page.getByLabel("Unit price").fill("5000000"); - - await page.getByLabel("Discount").fill("1000000"); - - await page - .getByRole("button", { - name: "Create draft quotation", - }) - .click(); - - await expect(page).toHaveURL( - /\/work\/commerce-operations\/quotations\/QT-\d{4}-\d{4}$/, - ); - - const quotationId = page.url().split("/").pop(); - - expect(quotationId).toBeTruthy(); - - if (!quotationId) { - throw new Error( - "Quotation ID was not found after creating the quotation.", - ); - } - - await expect( - page.getByRole("button", { - name: "Send quotation", - }), - ).toBeVisible(); - - await page - .getByRole("button", { - name: "Send quotation", - }) - .click(); - - await expect( - page.getByRole("button", { - name: "Accept quotation", - }), - ).toBeVisible(); - - await page - .getByRole("button", { - name: "Accept quotation", - }) - .click(); - - const convertButton = page.getByRole("button", { - name: /Convert to order/i, - }); - - await expect(convertButton).toBeVisible(); - - await Promise.all([ - page.waitForURL(/\/work\/commerce-operations\/orders\/ORD-\d{4}-\d{4}$/, { - timeout: 20_000, - waitUntil: "commit", - }), - - convertButton.click(), - ]); - - const orderId = page.url().split("/").pop(); - - expect(orderId).toBeTruthy(); - - if (!orderId) { - throw new Error("Order ID was not found after converting the quotation."); - } - - expect(orderId).toMatch(/^ORD-\d{4}-\d{4}$/); - - await expect( - page.getByRole("heading", { - name: orderId, - }), - ).toBeVisible(); - - await expect( - page - .getByText(quotationId, { - exact: true, - }) - .first(), - ).toBeVisible(); - - await page - .getByRole("button", { - name: "Confirm order", - }) - .click(); - - await expect( - page.getByRole("button", { - name: "Start processing", - }), - ).toBeVisible(); - - await page - .getByRole("button", { - name: "Start processing", - }) - .click(); - - await expect( - page.getByRole("button", { - name: "Mark shipped", - }), - ).toBeVisible(); - - await page - .getByRole("button", { - name: "Mark shipped", - }) - .click(); - - await expect( - page.getByRole("button", { - name: "Complete order", - }), - ).toBeVisible(); - - await page - .getByRole("button", { - name: "Complete order", - }) - .click(); - - await expect( - page - .getByText("COMPLETED", { - exact: true, - }) - .first(), - ).toBeVisible(); - }); -}); From 8e8e7e60fbc54fdf71217f60718de53dd1270d41 Mon Sep 17 00:00:00 2001 From: cibss Date: Thu, 10 Sep 2026 18:37:53 +0700 Subject: [PATCH 3/4] style: favicon & CTA --- apps/customers/src/app/favicon.ico | Bin 25931 -> 0 bytes apps/customers/src/app/layout.tsx | 6 +- apps/orders/src/app/favicon.ico | Bin 25931 -> 0 bytes apps/orders/src/app/layout.tsx | 6 +- apps/orders/src/app/orders/[orderId]/page.tsx | 27 +++++---- apps/payments/src/app/favicon.ico | Bin 25931 -> 0 bytes apps/payments/src/app/layout.tsx | 6 +- .../src/app/payments/[paymentId]/page.tsx | 9 ++- apps/quotations/src/app/favicon.ico | Bin 25931 -> 0 bytes apps/quotations/src/app/layout.tsx | 6 +- .../src/app/quotations/[quotationId]/page.tsx | 18 +++--- .../src/app/quotations/new/page.tsx | 5 +- apps/shell/src/app/favicon.ico | Bin 25931 -> 0 bytes apps/shell/src/app/layout.tsx | 6 +- package-lock.json | 1 + packages/platform-ui/src/index.ts | 7 +++ packages/platform-ui/src/metadata.ts | 16 ++++++ packages/ui/package.json | 1 + packages/ui/src/SubmitButton.tsx | 53 +++++------------- 19 files changed, 83 insertions(+), 84 deletions(-) delete mode 100644 apps/customers/src/app/favicon.ico delete mode 100644 apps/orders/src/app/favicon.ico delete mode 100644 apps/payments/src/app/favicon.ico delete mode 100644 apps/quotations/src/app/favicon.ico delete mode 100644 apps/shell/src/app/favicon.ico create mode 100644 packages/platform-ui/src/metadata.ts diff --git a/apps/customers/src/app/favicon.ico b/apps/customers/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/apps/customers/src/app/layout.tsx b/apps/customers/src/app/layout.tsx index 219263a..8a8c079 100644 --- a/apps/customers/src/app/layout.tsx +++ b/apps/customers/src/app/layout.tsx @@ -1,11 +1,9 @@ +import { commerceMetadata } from "@commerce/platform-ui"; import type { Metadata } from "next"; import "./globals.css"; -export const metadata: Metadata = { - title: "Customers | Commerce Operations", - description: "Customer management domain for Commerce Operations.", -}; +export const metadata: Metadata = commerceMetadata; export default function RootLayout({ children, diff --git a/apps/orders/src/app/favicon.ico b/apps/orders/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/apps/orders/src/app/layout.tsx b/apps/orders/src/app/layout.tsx index bbf9ecf..8a8c079 100644 --- a/apps/orders/src/app/layout.tsx +++ b/apps/orders/src/app/layout.tsx @@ -1,11 +1,9 @@ +import { commerceMetadata } from "@commerce/platform-ui"; import type { Metadata } from "next"; import "./globals.css"; -export const metadata: Metadata = { - title: "Orders | Commerce Operations", - description: "Order operations domain for Commerce Operations.", -}; +export const metadata: Metadata = commerceMetadata; export default function RootLayout({ children, diff --git a/apps/orders/src/app/orders/[orderId]/page.tsx b/apps/orders/src/app/orders/[orderId]/page.tsx index 998c855..4df4f1f 100644 --- a/apps/orders/src/app/orders/[orderId]/page.tsx +++ b/apps/orders/src/app/orders/[orderId]/page.tsx @@ -5,7 +5,7 @@ import { Panel, PanelHeader, ProgressSteps, - buttonClassName, + SubmitButton, type BadgeVariant, type ProgressStep, } from "@commerce/ui"; @@ -141,12 +141,13 @@ export default async function OrderDetailPage({
- +
)} @@ -154,7 +155,9 @@ export default async function OrderDetailPage({
- + + Confirm order +
) : null} @@ -162,17 +165,17 @@ export default async function OrderDetailPage({
- +
) : null} @@ -180,7 +183,9 @@ export default async function OrderDetailPage({
- + + Mark shipped +
) : null} @@ -188,7 +193,9 @@ export default async function OrderDetailPage({
- + + Complete order +
) : null} diff --git a/apps/payments/src/app/favicon.ico b/apps/payments/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m diff --git a/apps/payments/src/app/layout.tsx b/apps/payments/src/app/layout.tsx index 12dc101..8a8c079 100644 --- a/apps/payments/src/app/layout.tsx +++ b/apps/payments/src/app/layout.tsx @@ -1,11 +1,9 @@ +import { commerceMetadata } from "@commerce/platform-ui"; import type { Metadata } from "next"; import "./globals.css"; -export const metadata: Metadata = { - title: "Payments | Commerce Operations", - description: "Payment operations domain for Commerce Operations.", -}; +export const metadata: Metadata = commerceMetadata; export default function RootLayout({ children, diff --git a/apps/payments/src/app/payments/[paymentId]/page.tsx b/apps/payments/src/app/payments/[paymentId]/page.tsx index da801d0..2d0b59c 100644 --- a/apps/payments/src/app/payments/[paymentId]/page.tsx +++ b/apps/payments/src/app/payments/[paymentId]/page.tsx @@ -28,7 +28,6 @@ export default async function PaymentDetailPage({ searchParams, }: PaymentDetailPageProps) { const { paymentId } = await params; - const { error } = await searchParams; const payment = await getPayment(paymentId); @@ -61,7 +60,7 @@ export default async function PaymentDetailPage({ Mark failed @@ -70,7 +69,7 @@ export default async function PaymentDetailPage({
- + Mark as paid @@ -81,7 +80,7 @@ export default async function PaymentDetailPage({
- + Refund payment @@ -140,7 +139,7 @@ export default async function PaymentDetailPage({ Related order

-

+

{payment.orderId}

diff --git a/apps/quotations/src/app/favicon.ico b/apps/quotations/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/apps/quotations/src/app/layout.tsx b/apps/quotations/src/app/layout.tsx index b977914..8a8c079 100644 --- a/apps/quotations/src/app/layout.tsx +++ b/apps/quotations/src/app/layout.tsx @@ -1,11 +1,9 @@ +import { commerceMetadata } from "@commerce/platform-ui"; import type { Metadata } from "next"; import "./globals.css"; -export const metadata: Metadata = { - title: "Quotations | Commerce Operations", - description: "Quotation management domain for Commerce Operations.", -}; +export const metadata: Metadata = commerceMetadata; export default function RootLayout({ children, diff --git a/apps/quotations/src/app/quotations/[quotationId]/page.tsx b/apps/quotations/src/app/quotations/[quotationId]/page.tsx index f839146..4ec487f 100644 --- a/apps/quotations/src/app/quotations/[quotationId]/page.tsx +++ b/apps/quotations/src/app/quotations/[quotationId]/page.tsx @@ -40,7 +40,6 @@ export default async function QuotationDetailPage({ searchParams, }: QuotationDetailPageProps) { const { quotationId } = await params; - const { error } = await searchParams; const quotation = await getQuotation(quotationId); @@ -86,7 +85,7 @@ export default async function QuotationDetailPage({
- + Send quotation @@ -101,7 +100,7 @@ export default async function QuotationDetailPage({ value={quotation.id} /> - + Reject @@ -113,7 +112,7 @@ export default async function QuotationDetailPage({ value={quotation.id} /> - + Accept quotation @@ -124,7 +123,7 @@ export default async function QuotationDetailPage({
- + Convert to order → @@ -173,7 +172,9 @@ export default async function QuotationDetailPage({
@@ -256,8 +257,9 @@ export default async function QuotationDetailPage({

- This quotation was converted into
- + This quotation was converted into +
+ {quotation.convertedOrderId}
diff --git a/apps/quotations/src/app/quotations/new/page.tsx b/apps/quotations/src/app/quotations/new/page.tsx index d74d7f6..8114947 100644 --- a/apps/quotations/src/app/quotations/new/page.tsx +++ b/apps/quotations/src/app/quotations/new/page.tsx @@ -227,7 +227,10 @@ export default async function NewQuotationPage({

- + Create draft quotation
diff --git a/apps/shell/src/app/favicon.ico b/apps/shell/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/apps/shell/src/app/layout.tsx b/apps/shell/src/app/layout.tsx index 0b9566e..8a8c079 100644 --- a/apps/shell/src/app/layout.tsx +++ b/apps/shell/src/app/layout.tsx @@ -1,11 +1,9 @@ +import { commerceMetadata } from "@commerce/platform-ui"; import type { Metadata } from "next"; import "./globals.css"; -export const metadata: Metadata = { - title: "Commerce Operations", - description: "A B2B commerce operations platform built with micro-frontends.", -}; +export const metadata: Metadata = commerceMetadata; export default function RootLayout({ children, diff --git a/package-lock.json b/package-lock.json index bd27471..6e10a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10146,6 +10146,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.2", + "react-dom": "19.2.8", "typescript": "^5" }, "peerDependencies": { diff --git a/packages/platform-ui/src/index.ts b/packages/platform-ui/src/index.ts index 99b5b1d..8e6862e 100644 --- a/packages/platform-ui/src/index.ts +++ b/packages/platform-ui/src/index.ts @@ -4,3 +4,10 @@ export { createCommerceHref, type CommerceSection, } from "./CommerceShell"; + +export { + COMMERCE_FAVICON_URL, + COMMERCE_METADATA_DESCRIPTION, + COMMERCE_METADATA_TITLE, + commerceMetadata, +} from "./metadata"; diff --git a/packages/platform-ui/src/metadata.ts b/packages/platform-ui/src/metadata.ts new file mode 100644 index 0000000..6be33c7 --- /dev/null +++ b/packages/platform-ui/src/metadata.ts @@ -0,0 +1,16 @@ +export const COMMERCE_METADATA_TITLE = "Commerce Operations | Seiba Shonia"; + +export const COMMERCE_METADATA_DESCRIPTION = + "A B2B quote-to-cash operations platform covering quotations, orders, payments, customers, and fulfillment."; + +export const COMMERCE_FAVICON_URL = "https://seibashonia.dev/favicon.ico"; + +export const commerceMetadata = { + title: COMMERCE_METADATA_TITLE, + description: COMMERCE_METADATA_DESCRIPTION, + + icons: { + icon: COMMERCE_FAVICON_URL, + shortcut: COMMERCE_FAVICON_URL, + }, +}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 25e20de..a09115d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -20,6 +20,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.2", + "react-dom": "19.2.8", "typescript": "^5" } } diff --git a/packages/ui/src/SubmitButton.tsx b/packages/ui/src/SubmitButton.tsx index 9f36224..416d386 100644 --- a/packages/ui/src/SubmitButton.tsx +++ b/packages/ui/src/SubmitButton.tsx @@ -7,49 +7,22 @@ import { buttonClassName, type ButtonSize, type ButtonVariant } from "./Button"; type SubmitButtonProps = Omit< ButtonHTMLAttributes, - "children" | "type" + "children" | "className" > & { children: ReactNode; - pendingLabel: ReactNode; + pendingText: string; variant?: ButtonVariant; size?: ButtonSize; + className?: string; }; -function LoadingSpinner() { - return ( - - ); -} - export function SubmitButton({ children, - pendingLabel, + pendingText, variant = "primary", size = "md", className = "", - disabled, + disabled = false, ...props }: SubmitButtonProps) { const { pending } = useFormStatus(); @@ -62,16 +35,16 @@ export function SubmitButton({ type="submit" disabled={isDisabled} aria-busy={pending} - className={buttonClassName(variant, size, className)} + className={buttonClassName(variant, size, `gap-2 ${className}`)} > {pending ? ( - <> - - {pendingLabel} - - ) : ( - children - )} +