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
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
PaymentMethod,
|
||||
ApiResponse
|
||||
} from "~/types";
|
||||
import type { AlreadyCaptured } from "~/utils/captureResponse";
|
||||
|
||||
/**
|
||||
* Shop API composable.
|
||||
@@ -116,8 +117,9 @@ export function useShopApi() {
|
||||
});
|
||||
},
|
||||
|
||||
async capturePayment(uuid: string, paypalOrderId: string): Promise<Order> {
|
||||
return await $fetch<Order>(`/api/orders/${uuid}/capture/${paypalOrderId}`, {
|
||||
/** 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"
|
||||
});
|
||||
},
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
import { numberFormatter } from "~/utils/numberFormatter";
|
||||
import { trackEvent } from "~/utils/trackEvent";
|
||||
import { checkoutErrorMessageFor } from "~/utils/checkoutError";
|
||||
import { isAlreadyCaptured } from "~/utils/captureResponse";
|
||||
import type { Order } from "~/types";
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
@@ -312,10 +313,14 @@ async function initializePayPalButtons() {
|
||||
onApprove: async (data) => {
|
||||
try {
|
||||
// Capture payment server-side (CMS captures via PayPal server SDK and updates order)
|
||||
const capturedOrder = await shopApi.capturePayment(orderData.value.uuid, data.orderID!);
|
||||
const captured = await shopApi.capturePayment(orderData.value.uuid, data.orderID!);
|
||||
|
||||
if (capturedOrder.paymentAuthorised) {
|
||||
orderData.value = capturedOrder;
|
||||
if (isAlreadyCaptured(captured)) {
|
||||
// An earlier capture of this PayPal order went through: reload the paid order, keeping the shown one if that fails.
|
||||
orderData.value = await shopApi.getOrder(orderData.value.uuid).catch(() => orderData.value);
|
||||
await handleSuccessfulPayment();
|
||||
} else if (captured.paymentAuthorised) {
|
||||
orderData.value = captured;
|
||||
await handleSuccessfulPayment();
|
||||
} else {
|
||||
console.error("Payment capture did not result in authorisation");
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { isAlreadyCaptured } from "../../utils/captureResponse.ts";
|
||||
|
||||
test("recognises the CMS's answer to a repeated capture", () => {
|
||||
assert.equal(isAlreadyCaptured({ success: true, alreadyCaptured: true }), true);
|
||||
});
|
||||
|
||||
test("takes the paid order of a first capture for no repeated capture", () => {
|
||||
const order = { uuid: "11111111-2222-4333-8444-555555555555", total: 22.85, paymentAuthorised: true, email: "erika@example.org" };
|
||||
|
||||
assert.equal(isAlreadyCaptured(order), false);
|
||||
assert.equal(isAlreadyCaptured({ ...order, alreadyCaptured: false }), false);
|
||||
});
|
||||
|
||||
test("takes nothing but true for both fields as a repeated capture", () => {
|
||||
assert.equal(isAlreadyCaptured({ alreadyCaptured: true }), false);
|
||||
assert.equal(isAlreadyCaptured({ success: true, alreadyCaptured: "true" }), false);
|
||||
assert.equal(isAlreadyCaptured({ success: false, alreadyCaptured: true }), false);
|
||||
for (const response of [undefined, null, "alreadyCaptured", 1, [true]]) assert.equal(isAlreadyCaptured(response), false);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
// The answer of POST /api/orders/:uuid/capture/:paypalOrderId: the paid order, or, when the order is already paid with this
|
||||
// PayPal order (a repeated capture), { success: true, alreadyCaptured: true } (libreshop/cms src/api/order/controllers/order.ts).
|
||||
// Pure: no Nuxt or Vue imports, tested in tests/unit/captureResponse.test.ts.
|
||||
|
||||
/** The CMS's answer to a repeated capture: the payment went through before, and the order is paid. */
|
||||
export type AlreadyCaptured = { success: true; alreadyCaptured: true };
|
||||
|
||||
export const isAlreadyCaptured = (response: unknown): response is AlreadyCaptured =>
|
||||
typeof response === "object" &&
|
||||
response !== null &&
|
||||
(response as Record<string, unknown>).success === true &&
|
||||
(response as Record<string, unknown>).alreadyCaptured === true;
|
||||
Reference in New Issue
Block a user