@revenexx/cover 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/checkout/onepage/CheckoutPaymentSection.vue +39 -0
- package/app/composables/useCheckoutOnePage.ts +23 -2
- package/app/composables/usePspCheckout.ts +71 -0
- package/app/interfaces/payment.ts +2 -0
- package/app/payments/psp/registry.ts +19 -0
- package/app/payments/psp/stripe.ts +69 -0
- package/app/payments/psp/types.ts +29 -0
- package/i18n/locales/de/checkout.json +1 -0
- package/i18n/locales/en/checkout.json +1 -0
- package/package.json +1 -1
- package/server/api/orders/index.post.ts +52 -13
- package/server/api/payment/methods.post.ts +1 -0
- package/server/api/payment/psp-config.get.ts +40 -0
|
@@ -70,6 +70,38 @@ const showPoNumber = computed(() => form.value.paymentMethod === "po_number");
|
|
|
70
70
|
const showCostCenter = computed(() =>
|
|
71
71
|
form.value.paymentMethod === "cost_center"
|
|
72
72
|
&& (profile.value?.costCenters.length ?? 0) > 0);
|
|
73
|
+
|
|
74
|
+
// PSP card entry (Stripe Elements, …). The provider's secure field is mounted
|
|
75
|
+
// when a PSP card method is selected; the registry decides which provider needs
|
|
76
|
+
// one, so this is generic (Novalnet/Mollie become drop-ins).
|
|
77
|
+
const psp = usePspCheckout();
|
|
78
|
+
const selectedOption = computed<PaymentOption | null>(() =>
|
|
79
|
+
paymentOptions.value.find(o => o.method === form.value.paymentMethod) ?? null);
|
|
80
|
+
const needsCardField = computed(() => psp.requiresInstrument(selectedOption.value));
|
|
81
|
+
const cardEl = ref<HTMLElement | null>(null);
|
|
82
|
+
const cardError = ref<string | null>(null);
|
|
83
|
+
|
|
84
|
+
async function syncCardField(): Promise<void> {
|
|
85
|
+
cardError.value = null;
|
|
86
|
+
await nextTick();
|
|
87
|
+
if (needsCardField.value && cardEl.value && selectedOption.value) {
|
|
88
|
+
try {
|
|
89
|
+
await psp.mount(cardEl.value, selectedOption.value);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
cardError.value = err instanceof Error ? err.message : String(err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
psp.destroy();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (import.meta.client) {
|
|
101
|
+
watch([needsCardField, () => form.value.paymentMethod], () => void syncCardField());
|
|
102
|
+
onMounted(() => void syncCardField());
|
|
103
|
+
onBeforeUnmount(() => psp.destroy());
|
|
104
|
+
}
|
|
73
105
|
</script>
|
|
74
106
|
|
|
75
107
|
<template>
|
|
@@ -177,5 +209,12 @@ const showCostCenter = computed(() =>
|
|
|
177
209
|
:items="costCenterOptions"
|
|
178
210
|
/>
|
|
179
211
|
</div>
|
|
212
|
+
|
|
213
|
+
<!-- PSP secure card field (mounted client-side by the provider) -->
|
|
214
|
+
<div v-if="needsCardField" class="space-y-1.5">
|
|
215
|
+
<label class="block text-sm font-medium text-muted">{{ t('onepage.payment.cardDetails') }}</label>
|
|
216
|
+
<div ref="cardEl" class="w-full sm:max-w-sm p-3 rounded-lg border border-(--ui-border) bg-(--ui-bg)" />
|
|
217
|
+
<p v-if="cardError" class="text-sm text-error-500">{{ cardError }}</p>
|
|
218
|
+
</div>
|
|
180
219
|
</div>
|
|
181
220
|
</template>
|
|
@@ -55,6 +55,7 @@ export function useCheckoutOnePage() {
|
|
|
55
55
|
const { t } = useI18n();
|
|
56
56
|
const cart = useCartStore();
|
|
57
57
|
const { settings } = useB2BContext();
|
|
58
|
+
const psp = usePspCheckout();
|
|
58
59
|
const { data: profile, refresh: refreshProfile } = useCheckoutProfileSource();
|
|
59
60
|
const profileLoading = computed(() => profile.value === null);
|
|
60
61
|
|
|
@@ -315,12 +316,32 @@ export function useCheckoutOnePage() {
|
|
|
315
316
|
try {
|
|
316
317
|
await fetchSessionToken();
|
|
317
318
|
|
|
319
|
+
const payload = buildOrderPayload() as Record<string, unknown>;
|
|
320
|
+
// PSP card methods (Stripe Elements, …) tokenize client-side; only
|
|
321
|
+
// the single-use token reaches the BFF (PCI-safe). Self-managed and
|
|
322
|
+
// redirect methods carry no instrument here.
|
|
323
|
+
if (psp.hasActive()) {
|
|
324
|
+
const instrument = await psp.tokenize();
|
|
325
|
+
if (instrument) {
|
|
326
|
+
payload.paymentInstrument = instrument;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
318
330
|
const order = await $fetch(checkoutApi.orders(), {
|
|
319
331
|
method: "POST",
|
|
320
|
-
body:
|
|
321
|
-
}) as { orderId: string; approvalRequired?: boolean; approvers?: string[] };
|
|
332
|
+
body: payload,
|
|
333
|
+
}) as { orderId: string; approvalRequired?: boolean; approvers?: string[]; requiresAction?: boolean; redirectUrl?: string };
|
|
334
|
+
|
|
335
|
+
// Redirect-based PSPs (3DS step-up, wallets, …) — one generic
|
|
336
|
+
// handler for every provider: hand the buyer to the PSP, return later.
|
|
337
|
+
if (order.requiresAction && order.redirectUrl) {
|
|
338
|
+
cart.clearCart();
|
|
339
|
+
window.location.href = order.redirectUrl;
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
322
342
|
|
|
323
343
|
cart.clearCart();
|
|
344
|
+
psp.destroy();
|
|
324
345
|
return {
|
|
325
346
|
orderId: order.orderId,
|
|
326
347
|
approvalRequired: order.approvalRequired ?? false,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { pspTokenizerFor } from "../payments/psp/registry";
|
|
2
|
+
import type { PspTokenizer } from "../payments/psp/types";
|
|
3
|
+
import type { PaymentOption } from "../interfaces/payment";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Drives the per-PSP client-side tokenization in the checkout. The active
|
|
7
|
+
* tokenizer is a module-level singleton so the payment section (which mounts
|
|
8
|
+
* the secure card field) and the submit flow (which tokenizes on place-order)
|
|
9
|
+
* share one instance. Provider-agnostic: which PSP needs a field comes from
|
|
10
|
+
* the registry + /api/payment/psp-config.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface PspConfig {
|
|
14
|
+
providers: Record<string, { publishableKey: string; testMode: boolean }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Client-only imperative handle; never used during SSR.
|
|
18
|
+
let active: PspTokenizer | null = null;
|
|
19
|
+
|
|
20
|
+
export function usePspCheckout() {
|
|
21
|
+
const { locale } = useI18n();
|
|
22
|
+
const { data: config } = useFetch<PspConfig>("/api/payment/psp-config", {
|
|
23
|
+
key: "psp-config",
|
|
24
|
+
default: () => ({ providers: {} }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** True when this method needs inline tokenization (PSP + registered + has a public key). */
|
|
28
|
+
function requiresInstrument(option?: PaymentOption | null): boolean {
|
|
29
|
+
if (!option || option.kind !== "psp" || !option.provider) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!pspTokenizerFor(option.provider)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return Boolean(config.value?.providers[option.provider]?.publishableKey);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Mount the PSP's secure field into `el` for the chosen method. */
|
|
39
|
+
async function mount(el: HTMLElement, option: PaymentOption): Promise<void> {
|
|
40
|
+
destroy();
|
|
41
|
+
const factory = pspTokenizerFor(option.provider);
|
|
42
|
+
const pcfg = option.provider ? config.value?.providers[option.provider] : undefined;
|
|
43
|
+
if (!factory || !pcfg) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
active = factory();
|
|
47
|
+
await active.mount({
|
|
48
|
+
publishableKey: pcfg.publishableKey,
|
|
49
|
+
testMode: pcfg.testMode,
|
|
50
|
+
el,
|
|
51
|
+
locale: locale.value,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True when a secure field is mounted and a token must be produced on submit. */
|
|
56
|
+
function hasActive(): boolean {
|
|
57
|
+
return active !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Produce the single-use token from the mounted field (or null if none). */
|
|
61
|
+
async function tokenize(): Promise<{ token: string } | null> {
|
|
62
|
+
return active ? active.tokenize() : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function destroy(): void {
|
|
66
|
+
active?.destroy();
|
|
67
|
+
active = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { config, requiresInstrument, mount, hasActive, tokenize, destroy };
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createStripeTokenizer } from "./stripe";
|
|
2
|
+
import type { PspTokenizerFactory } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PSP tokenizer registry — the single extension point. A PSP that needs inline
|
|
6
|
+
* card collection gets a factory here; everything else (BFF token threading,
|
|
7
|
+
* the generic redirect/next_action handler, the order flow) is provider-
|
|
8
|
+
* agnostic. To add Novalnet/Mollie later: implement its tokenizer and add one
|
|
9
|
+
* line here.
|
|
10
|
+
*/
|
|
11
|
+
export const pspTokenizers: Record<string, PspTokenizerFactory> = {
|
|
12
|
+
stripe: createStripeTokenizer,
|
|
13
|
+
// novalnet: createNovalnetTokenizer,
|
|
14
|
+
// mollie: createMollieTokenizer,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function pspTokenizerFor(provider: string | undefined): PspTokenizerFactory | undefined {
|
|
18
|
+
return provider ? pspTokenizers[provider] : undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { PspMountContext, PspTokenizer } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stripe tokenizer — loads Stripe.js (no npm dep; the CDN script is the
|
|
5
|
+
* supported integration) and mounts a Card Element. `tokenize()` creates a
|
|
6
|
+
* PaymentMethod (`pm_…`) client-side; only that id is sent to the BFF.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface StripeCardElement { mount: (el: HTMLElement) => void; unmount: () => void }
|
|
10
|
+
interface StripeElements { create: (type: string, options?: unknown) => StripeCardElement }
|
|
11
|
+
interface StripeInstance {
|
|
12
|
+
elements: (options?: unknown) => StripeElements;
|
|
13
|
+
createPaymentMethod: (params: unknown) => Promise<{ paymentMethod?: { id: string }; error?: { message: string } }>;
|
|
14
|
+
}
|
|
15
|
+
type StripeCtor = (key: string) => StripeInstance;
|
|
16
|
+
|
|
17
|
+
const STRIPE_JS = "https://js.stripe.com/v3";
|
|
18
|
+
|
|
19
|
+
function loadStripeJs(): Promise<StripeCtor> {
|
|
20
|
+
const w = window as unknown as { Stripe?: StripeCtor };
|
|
21
|
+
if (w.Stripe) {
|
|
22
|
+
return Promise.resolve(w.Stripe);
|
|
23
|
+
}
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const existing = document.querySelector<HTMLScriptElement>(`script[src="${STRIPE_JS}"]`);
|
|
26
|
+
const onload = () => w.Stripe ? resolve(w.Stripe) : reject(new Error("Stripe.js loaded but window.Stripe missing"));
|
|
27
|
+
if (existing) {
|
|
28
|
+
existing.addEventListener("load", onload, { once: true });
|
|
29
|
+
existing.addEventListener("error", () => reject(new Error("failed to load Stripe.js")), { once: true });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const script = document.createElement("script");
|
|
33
|
+
script.src = STRIPE_JS;
|
|
34
|
+
script.async = true;
|
|
35
|
+
script.addEventListener("load", onload, { once: true });
|
|
36
|
+
script.addEventListener("error", () => reject(new Error("failed to load Stripe.js")), { once: true });
|
|
37
|
+
document.head.appendChild(script);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createStripeTokenizer(): PspTokenizer {
|
|
42
|
+
let stripe: StripeInstance | null = null;
|
|
43
|
+
let card: StripeCardElement | null = null;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
async mount(ctx: PspMountContext) {
|
|
47
|
+
const Stripe = await loadStripeJs();
|
|
48
|
+
stripe = Stripe(ctx.publishableKey);
|
|
49
|
+
const elements = stripe.elements({ locale: ctx.locale });
|
|
50
|
+
card = elements.create("card", { hidePostalCode: true });
|
|
51
|
+
card.mount(ctx.el);
|
|
52
|
+
},
|
|
53
|
+
async tokenize() {
|
|
54
|
+
if (!stripe || !card) {
|
|
55
|
+
throw new Error("card field is not ready");
|
|
56
|
+
}
|
|
57
|
+
const { paymentMethod, error } = await stripe.createPaymentMethod({ type: "card", card });
|
|
58
|
+
if (error || !paymentMethod) {
|
|
59
|
+
throw new Error(error?.message ?? "could not tokenize card");
|
|
60
|
+
}
|
|
61
|
+
return { token: paymentMethod.id };
|
|
62
|
+
},
|
|
63
|
+
destroy() {
|
|
64
|
+
card?.unmount();
|
|
65
|
+
card = null;
|
|
66
|
+
stripe = null;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-PSP client-side tokenizer contract. The checkout collects card (or other
|
|
3
|
+
* sensitive instrument) data through the PSP's own secure field and turns it
|
|
4
|
+
* into a single-use token — the raw data never touches our servers (PCI-safe).
|
|
5
|
+
*
|
|
6
|
+
* One implementation per PSP that needs inline collection (Stripe → Elements).
|
|
7
|
+
* PSPs that only redirect need NO tokenizer — the generic next_action/redirect
|
|
8
|
+
* handler covers them. Add Novalnet/Mollie by adding a factory to the registry.
|
|
9
|
+
*/
|
|
10
|
+
export interface PspMountContext {
|
|
11
|
+
/** Public/publishable key for the PSP (from /api/payment/psp-config). */
|
|
12
|
+
publishableKey: string;
|
|
13
|
+
testMode: boolean;
|
|
14
|
+
/** Element the PSP mounts its secure card field into. */
|
|
15
|
+
el: HTMLElement;
|
|
16
|
+
/** UI locale, e.g. "de" / "en". */
|
|
17
|
+
locale?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PspTokenizer {
|
|
21
|
+
/** Mount the secure field into `ctx.el`; resolves once it is ready. */
|
|
22
|
+
mount: (ctx: PspMountContext) => Promise<void>;
|
|
23
|
+
/** Produce a single-use token (e.g. Stripe `pm_…`) from the mounted field. */
|
|
24
|
+
tokenize: () => Promise<{ token: string }>;
|
|
25
|
+
/** Tear the field down (on unmount / method change). */
|
|
26
|
+
destroy: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PspTokenizerFactory = () => PspTokenizer;
|
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
"poNumber": "Bestellnummer",
|
|
127
127
|
"poNumberPlaceholder": "Bestellnummer eingeben...",
|
|
128
128
|
"costCenter": "Kostenstelle",
|
|
129
|
+
"cardDetails": "Kartendaten",
|
|
129
130
|
"change": "Ändern",
|
|
130
131
|
"noMethod": "Keine Zahlungsmethode ausgewählt",
|
|
131
132
|
"confirm": "Bestätigen",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/cover",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Cover \u2014 revenexx design system for Nuxt. Distributed as a Nuxt layer: generic UI components, theming tokens and stores shared by the demo shop, custom storefronts and the Blokkli theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -32,6 +32,10 @@ interface OrderPayload {
|
|
|
32
32
|
partialDelivery?: boolean;
|
|
33
33
|
orderNote?: string;
|
|
34
34
|
promoCode?: string;
|
|
35
|
+
/** PSP single-use token from the checkout's client-side tokenizer (e.g. Stripe pm_…). */
|
|
36
|
+
paymentInstrument?: { token?: string };
|
|
37
|
+
/** Where the PSP returns the buyer after a redirect/3DS step-up. */
|
|
38
|
+
returnUrl?: string;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
function isNonEmptyString(v: unknown): v is string {
|
|
@@ -310,6 +314,9 @@ export default defineEventHandler(async (event) => {
|
|
|
310
314
|
// again; a declined payment fails the order with 402 (and cancels
|
|
311
315
|
// the already placed live order).
|
|
312
316
|
let livePaymentStatus: string | null = null;
|
|
317
|
+
// Set when the PSP needs buyer action (3DS step-up / redirect PSPs) —
|
|
318
|
+
// the checkout hands the buyer to redirectUrl and confirms on return.
|
|
319
|
+
let pendingAction: { paymentId: string; redirectUrl: string } | null = null;
|
|
313
320
|
if (livePayments) {
|
|
314
321
|
const totalRow = calculation.totals.find(row => row.key === "total");
|
|
315
322
|
const address = body.address as Record<string, unknown> | undefined;
|
|
@@ -317,23 +324,39 @@ export default defineEventHandler(async (event) => {
|
|
|
317
324
|
? address?.billing as Record<string, unknown> | undefined
|
|
318
325
|
: address;
|
|
319
326
|
const country = toIsoCountry(body.billingCountry ?? billing?.country ?? "");
|
|
327
|
+
// The instrument (PSP token from the checkout's client-side tokenizer)
|
|
328
|
+
// is forwarded as-is; paymentsCreate's typed params drop unknown
|
|
329
|
+
// fields, so the create goes through the low-level transport.
|
|
330
|
+
const instrument = body.paymentInstrument as { token?: string } | undefined;
|
|
320
331
|
try {
|
|
321
|
-
const created = await useRevenexxSdk().
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
const created = await useRevenexxSdk().call<{ id: string; status: string; error_message?: string | null; next_action?: { type?: string; url?: string } | null }>(
|
|
333
|
+
"POST",
|
|
334
|
+
"/v1/payments",
|
|
335
|
+
{
|
|
336
|
+
body: {
|
|
337
|
+
method_code: payment.method,
|
|
338
|
+
// Charge exactly what the orders app computed (incl. shipping tax).
|
|
339
|
+
amount: Math.max(0, Math.round((liveOrderTotal ?? totalRow?.amount ?? 0) * 100) / 100),
|
|
340
|
+
currency: selectedCurrency ?? calculation.currency,
|
|
341
|
+
country,
|
|
342
|
+
order_ref: orderId,
|
|
343
|
+
idempotency_key: body.checkoutSessionToken,
|
|
344
|
+
...(instrument?.token ? { token: instrument.token } : {}),
|
|
345
|
+
...(body.returnUrl ? { return_url: body.returnUrl } : {}),
|
|
346
|
+
metadata: {
|
|
347
|
+
...(payment.poNumber ? { po_number: payment.poNumber } : {}),
|
|
348
|
+
...(payment.costCenter ? { cost_center: payment.costCenter } : {}),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
332
351
|
},
|
|
333
|
-
|
|
352
|
+
);
|
|
334
353
|
if (created.status === "failed") {
|
|
335
354
|
throw createError({ status: 402, message: created.error_message ?? "Payment was declined" });
|
|
336
355
|
}
|
|
356
|
+
if (created.status === "requires_action" && created.next_action?.url) {
|
|
357
|
+
// Order stays placed (payment pending); buyer completes at the PSP.
|
|
358
|
+
pendingAction = { paymentId: created.id, redirectUrl: created.next_action.url };
|
|
359
|
+
}
|
|
337
360
|
livePaymentStatus = created.status;
|
|
338
361
|
}
|
|
339
362
|
catch (err) {
|
|
@@ -359,8 +382,10 @@ export default defineEventHandler(async (event) => {
|
|
|
359
382
|
}
|
|
360
383
|
|
|
361
384
|
// The payment dimension of the live order follows the payment outcome.
|
|
385
|
+
// The payments app's terminal "paid" state is `captured` (PSP auto/
|
|
386
|
+
// manual capture); `succeeded`/`paid` are accepted as synonyms.
|
|
362
387
|
if (liveOrderUuid && livePaymentStatus) {
|
|
363
|
-
const mapped =
|
|
388
|
+
const mapped = ["captured", "succeeded", "paid"].includes(livePaymentStatus)
|
|
364
389
|
? OrderPaymentStatus.Paid
|
|
365
390
|
: livePaymentStatus === "authorized" ? OrderPaymentStatus.Authorized : OrderPaymentStatus.Pending;
|
|
366
391
|
try {
|
|
@@ -437,6 +462,20 @@ export default defineEventHandler(async (event) => {
|
|
|
437
462
|
}
|
|
438
463
|
}
|
|
439
464
|
|
|
465
|
+
// PSP needs buyer action (3DS / redirect PSPs): hand the redirect to the
|
|
466
|
+
// checkout. The order is placed; the payment finalizes on return.
|
|
467
|
+
if (pendingAction) {
|
|
468
|
+
return {
|
|
469
|
+
orderId,
|
|
470
|
+
status: "requires-action",
|
|
471
|
+
requiresAction: true,
|
|
472
|
+
redirectUrl: pendingAction.redirectUrl,
|
|
473
|
+
paymentId: pendingAction.paymentId,
|
|
474
|
+
createdAt: new Date().toISOString(),
|
|
475
|
+
payment,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
440
479
|
return {
|
|
441
480
|
orderId,
|
|
442
481
|
status: blockingApprovals.length ? "approval-pending" : "confirmed",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public PSP config for the checkout's client-side tokenizers — ONLY the
|
|
3
|
+
* publishable/public keys of enabled PSP providers, never the secret keys.
|
|
4
|
+
* The per-PSP frontend registry (see app/payments/psp) reads this to know
|
|
5
|
+
* which provider needs client-side tokenization and with which public key.
|
|
6
|
+
*
|
|
7
|
+
* Shape: { providers: { stripe: { publishableKey, testMode }, … } }
|
|
8
|
+
* Adding Novalnet/Mollie later = they appear here automatically once enabled
|
|
9
|
+
* with a publishable key on the provider; the FE registry gets a new entry.
|
|
10
|
+
*/
|
|
11
|
+
interface ApiProviderRow {
|
|
12
|
+
provider: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
test_mode: boolean;
|
|
15
|
+
credentials: Record<string, unknown> | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default defineEventHandler(async (event) => {
|
|
19
|
+
if (resolvePaymentServiceKey(event) !== "api") {
|
|
20
|
+
return { providers: {} as Record<string, { publishableKey: string; testMode: boolean }> };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const { items } = await useRevenexxSdk().call<{ items: ApiProviderRow[] }>("GET", "/v1/payments/providers");
|
|
24
|
+
const providers: Record<string, { publishableKey: string; testMode: boolean }> = {};
|
|
25
|
+
for (const row of items) {
|
|
26
|
+
if (!row.enabled) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const pk = row.credentials?.publishable_key;
|
|
30
|
+
if (typeof pk === "string" && pk.length > 0) {
|
|
31
|
+
providers[row.provider] = { publishableKey: pk, testMode: row.test_mode !== false };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { providers };
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
getLogService().error("Service error: payment/psp-config", apiErrorContext(err));
|
|
38
|
+
return { providers: {} as Record<string, { publishableKey: string; testMode: boolean }> };
|
|
39
|
+
}
|
|
40
|
+
});
|