Files
shop/composables/useShopApi.ts
libretech 6f434c21ba fix(checkout): take a repeated capture as a successful payment
The CMS answers a capture of an order already paid with this PayPal order
with { success: true, alreadyCaptured: true } instead of the order. Step 3
took that answer for a capture without authorisation and showed a payment
error. It now reloads the paid order (keeping the shown one if the reload
fails) and continues as after a first capture: confirmation, the
checkout-payment-completed event, and the redirect to the order's result
page. capturePayment is typed as Order | AlreadyCaptured, and the pure
isAlreadyCaptured (utils/captureResponse.ts) tells them apart.

Refs libretech/mp#71
2026-10-09 02:32:38 +02:00

150 lines
4.5 KiB
TypeScript

import type {
Product,
Order,
ProductVariantResponse,
PatternVariantsResponse,
ProductCover,
Legal,
DeliveryMethod,
PaymentMethod,
ApiResponse
} from "~/types";
import type { AlreadyCaptured } from "~/utils/captureResponse";
/**
* Shop API composable.
* All requests go through dedicated Nuxt server API routes.
* The API token is handled server-side and never exposed to clients.
*/
export function useShopApi() {
return {
// Products
async getProducts(coverId?: string, page = 1, pageSize = 24) {
const params = new URLSearchParams();
if (coverId) params.append("cover", coverId);
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
return await $fetch<ApiResponse<Product>>(`/api/products?${params}`);
},
async getCheapestProducts(coverId?: string, page = 1, pageSize = 24) {
const params = new URLSearchParams();
if (coverId) params.append("cover", coverId);
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
return await $fetch<ApiResponse<Product>>(`/api/products/promo?${params}`);
},
async getProductById(id: string) {
return await $fetch<Product>(`/api/products/${id}`);
},
async getProductBySlug(slug: string) {
return await $fetch<Product | null>(`/api/product/${slug}`);
},
async getProductVariants(id: string): Promise<ProductVariantResponse> {
return await $fetch<ProductVariantResponse>(`/api/products/${id}/variants`);
},
async getProductVariantsByProductId(id: number): Promise<ProductVariantResponse> {
return await $fetch<ProductVariantResponse>(`/api/products/${id}/variants`);
},
async getPatternVariants(id: string): Promise<PatternVariantsResponse> {
return await $fetch<PatternVariantsResponse>(`/api/products/${id}/variants/pattern`);
},
async getPatternVariantsByProductId(id: number): Promise<PatternVariantsResponse> {
return await $fetch<PatternVariantsResponse>(`/api/products/${id}/variants/pattern`);
},
// Product metadata
async getProductRulings() {
return await $fetch<ApiResponse<unknown>>(`/api/product-rulings`);
},
async getProductPatterns() {
return await $fetch<ApiResponse<unknown>>(`/api/product-patterns`);
},
async getProductPages() {
return await $fetch<ApiResponse<unknown>>(`/api/product-pages`);
},
async getProductCovers() {
return await $fetch<ApiResponse<ProductCover>>(`/api/product-covers`);
},
async getProductCoverById(id: string) {
return await $fetch<ApiResponse<ProductCover>>(`/api/product-covers/${id}`);
},
// Orders
async createOrder(): Promise<Order> {
return await $fetch<Order>("/api/orders", { method: "POST" });
},
async getOrder(uuid: string): Promise<Order> {
return await $fetch<Order>(`/api/orders/${uuid}`);
},
async updateOrder(uuid: string, data: Partial<Order>): Promise<Order> {
return await $fetch<Order>(`/api/orders/${uuid}`, {
method: "PUT",
body: { data }
});
},
async addProductToCart(uuid: string, productId: number, count = 1): Promise<Order> {
return await $fetch<Order>(`/api/orders/${uuid}/add-product/${productId}?count=${count}`, {
method: "PUT"
});
},
async removeProductFromCart(uuid: string, productId: number, count = 1): Promise<Order> {
return await $fetch<Order>(`/api/orders/${uuid}/remove-product/${productId}?count=${count}`, {
method: "PUT"
});
},
async checkoutOrder(uuid: string): Promise<{ id: string }> {
const returnUrl = import.meta.client ? window.location.href : "";
return await $fetch<{ id: string }>(`/api/orders/${uuid}/checkout?returnUrl=${encodeURIComponent(returnUrl)}`, {
method: "POST"
});
},
/** The paid order, or { success: true, alreadyCaptured: true } when an earlier capture of this PayPal order went through. */
async capturePayment(uuid: string, paypalOrderId: string): Promise<Order | AlreadyCaptured> {
return await $fetch<Order | AlreadyCaptured>(`/api/orders/${uuid}/capture/${paypalOrderId}`, {
method: "POST"
});
},
// Payment & Delivery
async getPaymentMethods() {
return await $fetch<ApiResponse<PaymentMethod>>(`/api/payments`);
},
async getDeliveryMethods() {
return await $fetch<ApiResponse<DeliveryMethod>>(`/api/deliveries`);
},
// Content
async getLegal(): Promise<Legal> {
return await $fetch<Legal>(`/api/legal`);
},
async getContent(): Promise<unknown> {
return await $fetch<unknown>(`/api/content`);
},
async getWebsites(): Promise<unknown> {
return await $fetch<unknown>(`/api/websites`);
}
};
}