Skip to content

Commit 503234e

Browse files
committed
feat: implement order management features including payment capture, refund, and cancellation actions
1 parent f45495c commit 503234e

6 files changed

Lines changed: 746 additions & 16 deletions

File tree

src/lib/server/api/commercify.ts

Lines changed: 225 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ import {
5252
type AppliedDiscountDTO,
5353
type CompleteCheckoutRequest,
5454
type CheckoutCompleteResponse,
55-
type OrderSummaryDTO
55+
type OrderSummaryDTO,
56+
type PaymentDetails
5657
} from './index';
5758
import type {
5859
Address,
@@ -63,7 +64,6 @@ import type {
6364
} from '$lib/types/checkout';
6465
import type { CreateDiscount, Discount } from '$lib/types/discount';
6566
import type { CommercifyUser } from '$lib/types/user';
66-
import { boolean } from 'zod/v4';
6767

6868
export class CommercifyClient {
6969
private baseUrl: string;
@@ -462,6 +462,8 @@ export class CommercifyClient {
462462
try {
463463
const response = await this.request<ResponseDTO<ProductDTO>>(`/products/${productId}`);
464464

465+
console.log('Response from getProduct:', response.data?.variants);
466+
465467
if (response.success && response.data) {
466468
return {
467469
success: true,
@@ -1038,14 +1040,25 @@ export class CommercifyClient {
10381040
* Map API variant response to our ProductVariant interface
10391041
*/
10401042
private mapApiVariantToVariant(apiVariant: VariantDTO): ProductVariant {
1041-
// Convert attributes array back to key-value object
1043+
// Handle attributes - they can come in different formats
10421044
const attributes: { [key: string]: string } = {};
1043-
if (apiVariant.attributes && Array.isArray(apiVariant.attributes)) {
1044-
apiVariant.attributes.forEach((attr: any) => {
1045-
if (attr.name && attr.value) {
1046-
attributes[attr.name] = attr.value;
1045+
1046+
if (apiVariant.attributes) {
1047+
if (Array.isArray(apiVariant.attributes)) {
1048+
// Handle array format: [{ name: 'Size', value: 'Large' }]
1049+
apiVariant.attributes.forEach((attr: any) => {
1050+
if (attr.name && attr.value) {
1051+
attributes[attr.name] = attr.value;
1052+
}
1053+
});
1054+
} else if (typeof apiVariant.attributes === 'object' && apiVariant.attributes !== null) {
1055+
// Handle object format: { Size: 'Large' }
1056+
for (const [key, value] of Object.entries(apiVariant.attributes)) {
1057+
if (typeof value === 'string') {
1058+
attributes[key] = value;
1059+
}
10471060
}
1048-
});
1061+
}
10491062
}
10501063

10511064
return {
@@ -1666,6 +1679,7 @@ export class CommercifyClient {
16661679
email: dto.customer.email,
16671680
phone: dto.customer.phone
16681681
},
1682+
createdAt: dto.created_at,
16691683
items: dto.items.map((item) => ({
16701684
productName: item.product_name,
16711685
variantName: item.variant_name,
@@ -1679,7 +1693,24 @@ export class CommercifyClient {
16791693
? this.mapShippingDetails(dto.shipping_details)
16801694
: undefined,
16811695
discountDetails: this.mapDiscountDetails(dto.discount_details),
1682-
paymentProvider: dto.payment_details?.provider
1696+
paymentProvider: dto.payment_details?.provider,
1697+
paymentDetails: dto.payment_details
1698+
? {
1699+
paymentId: dto.payment_details.payment_id || '',
1700+
method: dto.payment_details.method as 'credit_card' | 'wallet',
1701+
status: dto.payment_details.status as PaymentStatus,
1702+
captured: dto.payment_details.status === 'captured',
1703+
refunded: dto.payment_details.status === 'refunded',
1704+
amount: {
1705+
amount: dto.total_amount,
1706+
currency: dto.currency
1707+
},
1708+
provider: dto.payment_details.provider as 'stripe' | 'mobilepay',
1709+
events: [], // Events would need to be mapped if available in the DTO
1710+
createdAt: dto.created_at,
1711+
updatedAt: dto.updated_at
1712+
}
1713+
: undefined
16831714
};
16841715
}
16851716

@@ -1707,10 +1738,46 @@ export class CommercifyClient {
17071738
async getOrders(
17081739
params: OrderSearchRequest
17091740
): Promise<{ success: boolean; data?: PaginatedData<OrderSummary>; error?: string }> {
1710-
return {
1711-
success: false,
1712-
error: 'This method is not implemented yet. Please check back later.'
1713-
};
1741+
const queryParams = new URLSearchParams();
1742+
if (params.pagination.page) queryParams.append('page', String(params.pagination.page));
1743+
if (params.pagination.page_size)
1744+
queryParams.append('pageSize', String(params.pagination.page_size));
1745+
// if (params.status) queryParams.append('status', params.status);
1746+
// if (params.payment_status) queryParams.append('paymentStatus', params.payment_status);
1747+
1748+
try {
1749+
const response = await this.request<ListResponseDTO<OrderSummaryDTO>>(
1750+
`/admin/orders?${queryParams.toString()}`
1751+
);
1752+
1753+
console.log('Orders response:', response);
1754+
1755+
if (!response.success || !response.data) {
1756+
return {
1757+
success: false,
1758+
error: response.error || 'Failed to retrieve order'
1759+
};
1760+
}
1761+
1762+
const orders = response.data.map((order) => this.mapOrderSummary(order));
1763+
const pagination = this.mapPaginationData(response.pagination);
1764+
return {
1765+
success: true,
1766+
data: {
1767+
items: orders,
1768+
pagination
1769+
}
1770+
};
1771+
} catch (error) {
1772+
console.error(
1773+
'Error fetching orders:',
1774+
error instanceof Error ? error.message : String(error)
1775+
);
1776+
return {
1777+
success: false,
1778+
error: 'Failed to fetch orders'
1779+
};
1780+
}
17141781
}
17151782

17161783
async getOrderById(orderId: string): Promise<{ success: boolean; data?: Order; error?: string }> {
@@ -1750,6 +1817,151 @@ export class CommercifyClient {
17501817
}
17511818
}
17521819

1820+
/**
1821+
* Capture an authorized payment for an order
1822+
*/
1823+
async captureOrderPayment(orderId: string): Promise<{
1824+
success: boolean;
1825+
data?: Order;
1826+
error?: string;
1827+
}> {
1828+
if (!orderId) {
1829+
return {
1830+
success: false,
1831+
error: 'Order ID is required'
1832+
};
1833+
}
1834+
1835+
try {
1836+
const response = await this.request<ResponseDTO<OrderDTO>>(
1837+
`/admin/orders/${orderId}/capture`,
1838+
{
1839+
method: 'POST'
1840+
}
1841+
);
1842+
1843+
if (!response.success || !response.data) {
1844+
return {
1845+
success: false,
1846+
error: response.error || 'Failed to capture payment'
1847+
};
1848+
}
1849+
1850+
return {
1851+
success: true,
1852+
data: this.mapOrder(response.data)
1853+
};
1854+
} catch (error) {
1855+
console.error(
1856+
`Error capturing payment for order ${orderId}:`,
1857+
error instanceof Error ? error.message : String(error)
1858+
);
1859+
return {
1860+
success: false,
1861+
error: `Failed to capture payment: ${
1862+
error instanceof Error ? error.message : String(error)
1863+
}`
1864+
};
1865+
}
1866+
}
1867+
1868+
/**
1869+
* Refund a captured payment for an order
1870+
*/
1871+
async refundOrderPayment(
1872+
orderId: string,
1873+
amount?: number
1874+
): Promise<{
1875+
success: boolean;
1876+
data?: Order;
1877+
error?: string;
1878+
}> {
1879+
if (!orderId) {
1880+
return {
1881+
success: false,
1882+
error: 'Order ID is required'
1883+
};
1884+
}
1885+
1886+
try {
1887+
const requestBody = amount ? { amount } : {};
1888+
const response = await this.request<ResponseDTO<OrderDTO>>(
1889+
`/admin/orders/${orderId}/refund`,
1890+
{
1891+
method: 'POST',
1892+
body: amount ? JSON.stringify(requestBody) : undefined
1893+
}
1894+
);
1895+
1896+
if (!response.success || !response.data) {
1897+
return {
1898+
success: false,
1899+
error: response.error || 'Failed to refund payment'
1900+
};
1901+
}
1902+
1903+
return {
1904+
success: true,
1905+
data: this.mapOrder(response.data)
1906+
};
1907+
} catch (error) {
1908+
console.error(
1909+
`Error refunding payment for order ${orderId}:`,
1910+
error instanceof Error ? error.message : String(error)
1911+
);
1912+
return {
1913+
success: false,
1914+
error: `Failed to refund payment: ${error instanceof Error ? error.message : String(error)}`
1915+
};
1916+
}
1917+
}
1918+
1919+
/**
1920+
* Cancel an order and its associated payment
1921+
*/
1922+
async cancelOrder(orderId: string): Promise<{
1923+
success: boolean;
1924+
data?: Order;
1925+
error?: string;
1926+
}> {
1927+
if (!orderId) {
1928+
return {
1929+
success: false,
1930+
error: 'Order ID is required'
1931+
};
1932+
}
1933+
1934+
try {
1935+
const response = await this.request<ResponseDTO<OrderDTO>>(
1936+
`/admin/orders/${orderId}/cancel`,
1937+
{
1938+
method: 'POST'
1939+
}
1940+
);
1941+
1942+
if (!response.success || !response.data) {
1943+
return {
1944+
success: false,
1945+
error: response.error || 'Failed to cancel order'
1946+
};
1947+
}
1948+
1949+
return {
1950+
success: true,
1951+
data: this.mapOrder(response.data)
1952+
};
1953+
} catch (error) {
1954+
console.error(
1955+
`Error cancelling order ${orderId}:`,
1956+
error instanceof Error ? error.message : String(error)
1957+
);
1958+
return {
1959+
success: false,
1960+
error: `Failed to cancel order: ${error instanceof Error ? error.message : String(error)}`
1961+
};
1962+
}
1963+
}
1964+
17531965
async createDiscount(input: CreateDiscount): Promise<{
17541966
success: boolean;
17551967
data?: Discount;

src/lib/types/order.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface Order extends Checkout {
1010
checkoutId: string; // Reference to the original checkout session
1111
status: OrderStatus;
1212
paymentDetails?: PaymentDetails;
13+
createdAt: string; // ISO date string
1314
}
1415

1516
export interface OrderSummary {

src/lib/types/payment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface PaymentDetails {
1616
status: PaymentStatus; // e.g., 'pending', 'completed', 'failed'
1717
captured: boolean; // Whether the payment has been captured
1818
refunded: boolean; // Whether the payment has been refunded
19-
amout: Price;
19+
amount: Price;
2020
provider: PaymentProvider; // e.g., 'stripe', 'mobilepay'
2121
events: {
2222
type: 'charge' | 'capture' | 'refund';

src/routes/admin/orders/+page.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@
128128
</DropdownMenu.Trigger>
129129
<DropdownMenu.Content align="end">
130130
<DropdownMenu.Item>
131-
<Eye class="mr-2 h-4 w-4" />
132-
View Details
131+
<a href="/admin/orders/{order.id}" class="flex items-center w-full">
132+
<Eye class="mr-2 h-4 w-4" />
133+
View Details
134+
</a>
133135
</DropdownMenu.Item>
134136
<DropdownMenu.Item>
135137
<Package class="mr-2 h-4 w-4" />

0 commit comments

Comments
 (0)