includio-cms 0.15.0 → 0.15.2
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/CHANGELOG.md +61 -0
- package/DOCS.md +231 -1
- package/ROADMAP.md +7 -2
- package/dist/admin/client/shop/shipping-method-edit-page.svelte +1 -0
- package/dist/admin/client/shop/shipping-method-form.svelte +89 -21
- package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -1
- package/dist/admin/client/shop/shipping-method-new-page.svelte +1 -0
- package/dist/admin/client/shop/shipping-methods-list-page.svelte +7 -4
- package/dist/admin/client/shop/shop-products-list-page.svelte +2 -2
- package/dist/admin/components/fields/shop-field.svelte +63 -22
- package/dist/admin/remote/shop.remote.d.ts +16 -56
- package/dist/admin/remote/shop.remote.js +6 -4
- package/dist/cli/scaffold/admin.js +32 -0
- package/dist/db-postgres/schema/shop/order.d.ts +34 -0
- package/dist/db-postgres/schema/shop/order.js +2 -0
- package/dist/db-postgres/schema/shop/product.d.ts +4 -4
- package/dist/db-postgres/schema/shop/product.js +3 -2
- package/dist/db-postgres/schema/shop/productVariant.d.ts +4 -4
- package/dist/db-postgres/schema/shop/productVariant.js +3 -2
- package/dist/db-postgres/schema/shop/shippingMethod.d.ts +23 -4
- package/dist/db-postgres/schema/shop/shippingMethod.js +4 -2
- package/dist/shop/adapters/payu/client.d.ts +22 -0
- package/dist/shop/adapters/payu/client.js +78 -0
- package/dist/shop/adapters/payu/index.d.ts +24 -0
- package/dist/shop/adapters/payu/index.js +88 -0
- package/dist/shop/adapters/payu/payload.d.ts +48 -0
- package/dist/shop/adapters/payu/payload.js +48 -0
- package/dist/shop/adapters/payu/signature.d.ts +12 -0
- package/dist/shop/adapters/payu/signature.js +50 -0
- package/dist/shop/adapters/payu/status-map.d.ts +3 -0
- package/dist/shop/adapters/payu/status-map.js +14 -0
- package/dist/shop/cart/order-token-cookie.d.ts +9 -0
- package/dist/shop/cart/order-token-cookie.js +40 -0
- package/dist/shop/client/index.d.ts +64 -1
- package/dist/shop/client/index.js +9 -0
- package/dist/shop/client/use-order.svelte.d.ts +32 -0
- package/dist/shop/client/use-order.svelte.js +105 -0
- package/dist/shop/http/checkout-handler.js +47 -4
- package/dist/shop/http/index.d.ts +4 -0
- package/dist/shop/http/index.js +4 -0
- package/dist/shop/http/order-handler.d.ts +4 -0
- package/dist/shop/http/order-handler.js +85 -0
- package/dist/shop/http/refresh-payment-handler.d.ts +4 -0
- package/dist/shop/http/refresh-payment-handler.js +73 -0
- package/dist/shop/http/retry-payment-handler.d.ts +4 -0
- package/dist/shop/http/retry-payment-handler.js +99 -0
- package/dist/shop/http/retry-payment-logic.d.ts +2 -0
- package/dist/shop/http/retry-payment-logic.js +4 -0
- package/dist/shop/http/shipping-handler.js +2 -1
- package/dist/shop/http/webhook-handler.d.ts +4 -0
- package/dist/shop/http/webhook-handler.js +73 -0
- package/dist/shop/http/webhook-logic.d.ts +4 -0
- package/dist/shop/http/webhook-logic.js +21 -0
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +3 -1
- package/dist/shop/pricing.d.ts +4 -0
- package/dist/shop/pricing.js +18 -0
- package/dist/shop/server/cart-hydrate.js +6 -3
- package/dist/shop/server/email.js +18 -2
- package/dist/shop/server/order-access-url.d.ts +7 -0
- package/dist/shop/server/order-access-url.js +6 -0
- package/dist/shop/server/orders.d.ts +1 -0
- package/dist/shop/server/orders.js +12 -0
- package/dist/shop/server/payment-compat.d.ts +5 -0
- package/dist/shop/server/payment-compat.js +9 -0
- package/dist/shop/server/populate.d.ts +2 -0
- package/dist/shop/server/shipping.d.ts +12 -4
- package/dist/shop/server/shipping.js +24 -14
- package/dist/shop/server/shop-data.d.ts +8 -2
- package/dist/shop/server/shop-data.js +18 -10
- package/dist/shop/svelte/OrderStatus.svelte +368 -0
- package/dist/shop/svelte/OrderStatus.svelte.d.ts +14 -0
- package/dist/shop/svelte/index.d.ts +3 -0
- package/dist/shop/svelte/index.js +2 -0
- package/dist/shop/svelte/labels.d.ts +25 -0
- package/dist/shop/svelte/labels.js +41 -0
- package/dist/shop/types.d.ts +19 -1
- package/dist/updates/0.15.1/index.d.ts +2 -0
- package/dist/updates/0.15.1/index.js +27 -0
- package/dist/updates/0.15.2/index.d.ts +2 -0
- package/dist/updates/0.15.2/index.js +18 -0
- package/dist/updates/index.js +3 -1
- package/package.json +5 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { json } from '@sveltejs/kit';
|
|
2
2
|
import { getCMS } from '../../core/cms.js';
|
|
3
3
|
import { readCartCookie, writeCartCookie } from '../cart/cookie.js';
|
|
4
|
-
import {
|
|
4
|
+
import { writeOrderTokenCookie } from '../cart/order-token-cookie.js';
|
|
5
|
+
import { createOrderFromCart, setPaymentProviderRef } from '../server/orders.js';
|
|
5
6
|
import { checkRateLimit, clientKey } from '../rate-limit.js';
|
|
7
|
+
import { requireShopConfig } from '../server/db.js';
|
|
6
8
|
function shopEnabled() {
|
|
7
9
|
try {
|
|
8
10
|
return getCMS().shopConfig !== null;
|
|
@@ -41,7 +43,7 @@ function asConsents(v) {
|
|
|
41
43
|
}
|
|
42
44
|
export function createCheckoutHandler() {
|
|
43
45
|
return {
|
|
44
|
-
POST: async ({ request, cookies }) => {
|
|
46
|
+
POST: async ({ request, cookies, getClientAddress }) => {
|
|
45
47
|
if (!shopEnabled())
|
|
46
48
|
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
47
49
|
const rule = getCMS().shopConfig?.rateLimit.checkout ?? { limit: 5, windowSec: 60 };
|
|
@@ -81,14 +83,55 @@ export function createCheckoutHandler() {
|
|
|
81
83
|
});
|
|
82
84
|
// Clear cart on successful order
|
|
83
85
|
writeCartCookie(cookies, []);
|
|
86
|
+
// Short-lived cookie for same-browser order view fallback
|
|
87
|
+
writeOrderTokenCookie(cookies, {
|
|
88
|
+
number: result.order.number,
|
|
89
|
+
token: result.order.accessToken
|
|
90
|
+
});
|
|
91
|
+
// Initiate payment (if adapter supports redirect)
|
|
92
|
+
const shop = requireShopConfig();
|
|
93
|
+
const adapter = shop.payment.find((a) => a.id === paymentMethod);
|
|
94
|
+
let paymentResult = null;
|
|
95
|
+
if (adapter) {
|
|
96
|
+
const orderRef = {
|
|
97
|
+
id: result.order.id,
|
|
98
|
+
number: result.order.number,
|
|
99
|
+
totalGross: result.order.totalGross,
|
|
100
|
+
currency: shop.currency,
|
|
101
|
+
customerEmail: result.order.customerEmail
|
|
102
|
+
};
|
|
103
|
+
let customerIp;
|
|
104
|
+
try {
|
|
105
|
+
customerIp = getClientAddress();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
customerIp = undefined;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
paymentResult = await adapter.createPayment(orderRef, {
|
|
112
|
+
customerIp,
|
|
113
|
+
language: result.order.language
|
|
114
|
+
});
|
|
115
|
+
if (paymentResult.providerRef) {
|
|
116
|
+
await setPaymentProviderRef(result.order.id, paymentResult.providerRef);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`[shop] createPayment failed for ${result.order.number}:`, err);
|
|
121
|
+
paymentResult = { status: 'error' };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const requiresRedirect = paymentResult?.status === 'redirect';
|
|
84
125
|
return json({
|
|
85
126
|
orderId: result.order.id,
|
|
86
127
|
orderNumber: result.order.number,
|
|
128
|
+
accessToken: result.order.accessToken,
|
|
87
129
|
status: result.order.status,
|
|
88
130
|
totalGross: result.order.totalGross,
|
|
89
131
|
currency: result.order.currency,
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
paymentStatus: paymentResult?.status ?? 'manual',
|
|
133
|
+
requiresPaymentRedirect: requiresRedirect,
|
|
134
|
+
redirectUrl: requiresRedirect ? paymentResult?.redirectUrl ?? null : null
|
|
92
135
|
});
|
|
93
136
|
}
|
|
94
137
|
catch (err) {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { createCartHandler } from './cart-handler.js';
|
|
2
2
|
export { createShippingMethodsHandler } from './shipping-handler.js';
|
|
3
3
|
export { createCheckoutHandler } from './checkout-handler.js';
|
|
4
|
+
export { createOrderHandler } from './order-handler.js';
|
|
5
|
+
export { createPaymentWebhookHandler } from './webhook-handler.js';
|
|
6
|
+
export { createRefreshPaymentHandler } from './refresh-payment-handler.js';
|
|
7
|
+
export { createRetryPaymentHandler } from './retry-payment-handler.js';
|
package/dist/shop/http/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { createCartHandler } from './cart-handler.js';
|
|
2
2
|
export { createShippingMethodsHandler } from './shipping-handler.js';
|
|
3
3
|
export { createCheckoutHandler } from './checkout-handler.js';
|
|
4
|
+
export { createOrderHandler } from './order-handler.js';
|
|
5
|
+
export { createPaymentWebhookHandler } from './webhook-handler.js';
|
|
6
|
+
export { createRefreshPaymentHandler } from './refresh-payment-handler.js';
|
|
7
|
+
export { createRetryPaymentHandler } from './retry-payment-handler.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { getCMS } from '../../core/cms.js';
|
|
4
|
+
import { getOrderByNumber, getOrderItems, getOrderStatusHistory } from '../server/orders.js';
|
|
5
|
+
import { readOrderTokenCookie } from '../cart/order-token-cookie.js';
|
|
6
|
+
function shopEnabled() {
|
|
7
|
+
try {
|
|
8
|
+
return getCMS().shopConfig !== null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function tokensMatch(a, b) {
|
|
15
|
+
if (a.length !== b.length)
|
|
16
|
+
return false;
|
|
17
|
+
try {
|
|
18
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function createOrderHandler() {
|
|
25
|
+
return {
|
|
26
|
+
GET: async ({ params, url, cookies }) => {
|
|
27
|
+
if (!shopEnabled())
|
|
28
|
+
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
29
|
+
const number = params.number;
|
|
30
|
+
if (!number)
|
|
31
|
+
return json({ error: 'Order number required' }, { status: 400 });
|
|
32
|
+
const order = await getOrderByNumber(number);
|
|
33
|
+
if (!order)
|
|
34
|
+
return json({ error: 'Not found' }, { status: 404 });
|
|
35
|
+
const queryToken = url.searchParams.get('token') ?? '';
|
|
36
|
+
const cookieValue = readOrderTokenCookie(cookies);
|
|
37
|
+
const cookieToken = cookieValue && cookieValue.number === order.number ? cookieValue.token : '';
|
|
38
|
+
const providedToken = queryToken || cookieToken;
|
|
39
|
+
if (!providedToken || !tokensMatch(providedToken, order.accessToken)) {
|
|
40
|
+
return json({ error: 'Forbidden' }, { status: 403 });
|
|
41
|
+
}
|
|
42
|
+
const [items, history] = await Promise.all([
|
|
43
|
+
getOrderItems(order.id),
|
|
44
|
+
getOrderStatusHistory(order.id)
|
|
45
|
+
]);
|
|
46
|
+
return json({
|
|
47
|
+
order: {
|
|
48
|
+
id: order.id,
|
|
49
|
+
number: order.number,
|
|
50
|
+
status: order.status,
|
|
51
|
+
currency: order.currency,
|
|
52
|
+
customerEmail: order.customerEmail,
|
|
53
|
+
customerName: order.customerName,
|
|
54
|
+
customerPhone: order.customerPhone,
|
|
55
|
+
shippingAddress: order.shippingAddress,
|
|
56
|
+
totalNet: order.totalNet,
|
|
57
|
+
totalGross: order.totalGross,
|
|
58
|
+
vatAmount: order.vatAmount,
|
|
59
|
+
shippingNet: order.shippingNet,
|
|
60
|
+
shippingGross: order.shippingGross,
|
|
61
|
+
paymentMethod: order.paymentMethod,
|
|
62
|
+
carrierType: order.carrierType,
|
|
63
|
+
carrierRef: order.carrierRef,
|
|
64
|
+
language: order.language,
|
|
65
|
+
createdAt: order.createdAt
|
|
66
|
+
},
|
|
67
|
+
items: items.map((i) => ({
|
|
68
|
+
id: i.id,
|
|
69
|
+
variantId: i.variantId,
|
|
70
|
+
nameSnapshot: i.nameSnapshot,
|
|
71
|
+
skuSnapshot: i.skuSnapshot,
|
|
72
|
+
priceNetSnapshot: i.priceNetSnapshot,
|
|
73
|
+
priceGrossSnapshot: i.priceGrossSnapshot,
|
|
74
|
+
vatRate: i.vatRate,
|
|
75
|
+
qty: i.qty
|
|
76
|
+
})),
|
|
77
|
+
statusHistory: history.map((h) => ({
|
|
78
|
+
status: h.status,
|
|
79
|
+
note: h.note,
|
|
80
|
+
changedAt: h.changedAt
|
|
81
|
+
}))
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { getCMS } from '../../core/cms.js';
|
|
4
|
+
import { requireShopConfig } from '../server/db.js';
|
|
5
|
+
import { getOrderByNumber, updateOrderStatus } from '../server/orders.js';
|
|
6
|
+
import { readOrderTokenCookie } from '../cart/order-token-cookie.js';
|
|
7
|
+
import { isTerminalStatus, mapEventToStatus } from './webhook-logic.js';
|
|
8
|
+
function shopEnabled() {
|
|
9
|
+
try {
|
|
10
|
+
return getCMS().shopConfig !== null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function tokensMatch(a, b) {
|
|
17
|
+
if (a.length !== b.length)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createRefreshPaymentHandler() {
|
|
27
|
+
return {
|
|
28
|
+
POST: async ({ params, url, cookies }) => {
|
|
29
|
+
if (!shopEnabled())
|
|
30
|
+
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
31
|
+
const number = params.number;
|
|
32
|
+
if (!number)
|
|
33
|
+
return json({ error: 'Order number required' }, { status: 400 });
|
|
34
|
+
const order = await getOrderByNumber(number);
|
|
35
|
+
if (!order)
|
|
36
|
+
return json({ error: 'Not found' }, { status: 404 });
|
|
37
|
+
const queryToken = url.searchParams.get('token') ?? '';
|
|
38
|
+
const cookieValue = readOrderTokenCookie(cookies);
|
|
39
|
+
const cookieToken = cookieValue && cookieValue.number === order.number ? cookieValue.token : '';
|
|
40
|
+
const providedToken = queryToken || cookieToken;
|
|
41
|
+
if (!providedToken || !tokensMatch(providedToken, order.accessToken)) {
|
|
42
|
+
return json({ error: 'Forbidden' }, { status: 403 });
|
|
43
|
+
}
|
|
44
|
+
if (isTerminalStatus(order.status)) {
|
|
45
|
+
return json({ status: order.status, noop: true });
|
|
46
|
+
}
|
|
47
|
+
if (!order.paymentMethod || !order.paymentProviderRef) {
|
|
48
|
+
return json({ status: order.status, noop: true });
|
|
49
|
+
}
|
|
50
|
+
const shop = requireShopConfig();
|
|
51
|
+
const adapter = shop.payment.find((a) => a.id === order.paymentMethod);
|
|
52
|
+
if (!adapter || typeof adapter.getStatus !== 'function') {
|
|
53
|
+
return json({ status: order.status, noop: true });
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const event = await adapter.getStatus(order.paymentProviderRef);
|
|
57
|
+
const target = mapEventToStatus(event);
|
|
58
|
+
if (target && target !== order.status) {
|
|
59
|
+
await updateOrderStatus(order.id, target, {
|
|
60
|
+
note: `Refresh-payment (${order.paymentMethod})`,
|
|
61
|
+
changedBy: 'refresh-payment'
|
|
62
|
+
});
|
|
63
|
+
return json({ status: target, updated: true });
|
|
64
|
+
}
|
|
65
|
+
return json({ status: order.status, updated: false });
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(`[shop] refresh-payment failed for ${order.number}:`, err);
|
|
69
|
+
return json({ error: 'Refresh failed' }, { status: 502 });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { getCMS } from '../../core/cms.js';
|
|
4
|
+
import { requireShopConfig } from '../server/db.js';
|
|
5
|
+
import { getOrderByNumber, setPaymentProviderRef, updateOrderStatus } from '../server/orders.js';
|
|
6
|
+
import { readOrderTokenCookie } from '../cart/order-token-cookie.js';
|
|
7
|
+
import { canRetryPayment } from './retry-payment-logic.js';
|
|
8
|
+
function shopEnabled() {
|
|
9
|
+
try {
|
|
10
|
+
return getCMS().shopConfig !== null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function tokensMatch(a, b) {
|
|
17
|
+
if (a.length !== b.length)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function createRetryPaymentHandler() {
|
|
27
|
+
return {
|
|
28
|
+
POST: async ({ params, url, cookies, getClientAddress }) => {
|
|
29
|
+
if (!shopEnabled())
|
|
30
|
+
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
31
|
+
const number = params.number;
|
|
32
|
+
if (!number)
|
|
33
|
+
return json({ error: 'Order number required' }, { status: 400 });
|
|
34
|
+
const order = await getOrderByNumber(number);
|
|
35
|
+
if (!order)
|
|
36
|
+
return json({ error: 'Not found' }, { status: 404 });
|
|
37
|
+
const queryToken = url.searchParams.get('token') ?? '';
|
|
38
|
+
const cookieValue = readOrderTokenCookie(cookies);
|
|
39
|
+
const cookieToken = cookieValue && cookieValue.number === order.number ? cookieValue.token : '';
|
|
40
|
+
const providedToken = queryToken || cookieToken;
|
|
41
|
+
if (!providedToken || !tokensMatch(providedToken, order.accessToken)) {
|
|
42
|
+
return json({ error: 'Forbidden' }, { status: 403 });
|
|
43
|
+
}
|
|
44
|
+
if (!canRetryPayment(order.status)) {
|
|
45
|
+
return json({ error: `Order status "${order.status}" does not allow retry`, status: order.status }, { status: 409 });
|
|
46
|
+
}
|
|
47
|
+
if (!order.paymentMethod) {
|
|
48
|
+
return json({ error: 'Order has no payment method' }, { status: 400 });
|
|
49
|
+
}
|
|
50
|
+
const shop = requireShopConfig();
|
|
51
|
+
const adapter = shop.payment.find((a) => a.id === order.paymentMethod);
|
|
52
|
+
if (!adapter) {
|
|
53
|
+
return json({ error: 'Payment adapter not available' }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
const orderRef = {
|
|
56
|
+
id: order.id,
|
|
57
|
+
number: order.number,
|
|
58
|
+
totalGross: order.totalGross,
|
|
59
|
+
currency: shop.currency,
|
|
60
|
+
customerEmail: order.customerEmail
|
|
61
|
+
};
|
|
62
|
+
let customerIp;
|
|
63
|
+
try {
|
|
64
|
+
customerIp = getClientAddress();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
customerIp = undefined;
|
|
68
|
+
}
|
|
69
|
+
let result;
|
|
70
|
+
try {
|
|
71
|
+
result = await adapter.createPayment(orderRef, {
|
|
72
|
+
customerIp,
|
|
73
|
+
language: order.language
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`[shop] retry-payment failed for ${order.number}:`, err);
|
|
78
|
+
return json({ error: 'Payment creation failed' }, { status: 502 });
|
|
79
|
+
}
|
|
80
|
+
if (result.providerRef) {
|
|
81
|
+
await setPaymentProviderRef(order.id, result.providerRef);
|
|
82
|
+
}
|
|
83
|
+
// If the order was paymentRejected, move it back to awaitingPayment so the
|
|
84
|
+
// new attempt can transition it via webhook/polling.
|
|
85
|
+
if (order.status === 'paymentRejected') {
|
|
86
|
+
await updateOrderStatus(order.id, 'awaitingPayment', {
|
|
87
|
+
note: 'Retry payment',
|
|
88
|
+
changedBy: 'retry-payment'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return json({
|
|
92
|
+
status: result.status === 'redirect' ? 'awaitingPayment' : order.status,
|
|
93
|
+
paymentStatus: result.status,
|
|
94
|
+
requiresPaymentRedirect: result.status === 'redirect',
|
|
95
|
+
redirectUrl: result.status === 'redirect' ? result.redirectUrl ?? null : null
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
import { requireShopConfig } from '../server/db.js';
|
|
4
|
+
import { getOrderByNumber, updateOrderStatus } from '../server/orders.js';
|
|
5
|
+
import { checkRateLimit, clientKey } from '../rate-limit.js';
|
|
6
|
+
import { isTerminalStatus, mapEventToStatus } from './webhook-logic.js';
|
|
7
|
+
function shopEnabled() {
|
|
8
|
+
try {
|
|
9
|
+
return getCMS().shopConfig !== null;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function findAdapter(provider) {
|
|
16
|
+
const shop = requireShopConfig();
|
|
17
|
+
return shop.payment.find((a) => a.id === provider) ?? null;
|
|
18
|
+
}
|
|
19
|
+
export function createPaymentWebhookHandler() {
|
|
20
|
+
return {
|
|
21
|
+
POST: async ({ params, request }) => {
|
|
22
|
+
if (!shopEnabled())
|
|
23
|
+
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
24
|
+
const provider = params.provider;
|
|
25
|
+
if (!provider)
|
|
26
|
+
return json({ error: 'Provider required' }, { status: 400 });
|
|
27
|
+
const rule = getCMS().shopConfig?.rateLimit.webhook ?? { limit: 60, windowSec: 60 };
|
|
28
|
+
const rl = checkRateLimit(clientKey(request, `webhook:${provider}`), rule);
|
|
29
|
+
if (!rl.allowed) {
|
|
30
|
+
return json({ error: 'Rate limit exceeded' }, { status: 429 });
|
|
31
|
+
}
|
|
32
|
+
const adapter = findAdapter(provider);
|
|
33
|
+
if (!adapter || typeof adapter.handleWebhook !== 'function') {
|
|
34
|
+
return json({ error: 'Unknown provider' }, { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
let event;
|
|
37
|
+
try {
|
|
38
|
+
event = await adapter.handleWebhook(request);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(`[shop] Webhook parse/verify failed for ${provider}:`, err);
|
|
42
|
+
// Bad signature / malformed body → 400 (don't encourage retries with 200)
|
|
43
|
+
return json({ error: 'Invalid webhook' }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
const order = await getOrderByNumber(event.orderNumber);
|
|
46
|
+
if (!order) {
|
|
47
|
+
// Unknown order — 200 so the provider stops retrying
|
|
48
|
+
console.warn(`[shop] Webhook for unknown order ${event.orderNumber} (${provider}) — acking.`);
|
|
49
|
+
return json({ received: true });
|
|
50
|
+
}
|
|
51
|
+
// Idempotency — terminal states are no-ops
|
|
52
|
+
if (isTerminalStatus(order.status)) {
|
|
53
|
+
return json({ received: true, idempotent: true });
|
|
54
|
+
}
|
|
55
|
+
const targetStatus = mapEventToStatus(event);
|
|
56
|
+
if (!targetStatus) {
|
|
57
|
+
return json({ received: true, noop: true });
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await updateOrderStatus(order.id, targetStatus, {
|
|
61
|
+
note: `Payment webhook (${provider})`,
|
|
62
|
+
changedBy: 'payment-webhook'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error(`[shop] updateOrderStatus failed for ${order.number}:`, err);
|
|
67
|
+
// Return 500 — provider retry helps
|
|
68
|
+
return json({ error: 'Processing failed' }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
return json({ received: true });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OrderStatus, PaymentEvent } from '../types.js';
|
|
2
|
+
export declare const TERMINAL_ORDER_STATUSES: ReadonlySet<OrderStatus>;
|
|
3
|
+
export declare function isTerminalStatus(status: OrderStatus): boolean;
|
|
4
|
+
export declare function mapEventToStatus(event: PaymentEvent): OrderStatus | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const TERMINAL_ORDER_STATUSES = new Set([
|
|
2
|
+
'paid',
|
|
3
|
+
'paymentRejected',
|
|
4
|
+
'cancelled',
|
|
5
|
+
'done'
|
|
6
|
+
]);
|
|
7
|
+
export function isTerminalStatus(status) {
|
|
8
|
+
return TERMINAL_ORDER_STATUSES.has(status);
|
|
9
|
+
}
|
|
10
|
+
export function mapEventToStatus(event) {
|
|
11
|
+
switch (event.status) {
|
|
12
|
+
case 'paid':
|
|
13
|
+
return 'paid';
|
|
14
|
+
case 'paymentRejected':
|
|
15
|
+
return 'paymentRejected';
|
|
16
|
+
case 'pending':
|
|
17
|
+
return null;
|
|
18
|
+
default:
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/shop/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ShopConfig, ResolvedShopConfig } from './types.js';
|
|
2
2
|
export declare function defineShop(config: ShopConfig): ResolvedShopConfig;
|
|
3
3
|
export { manualAdapter } from './adapters/manual/index.js';
|
|
4
|
-
export
|
|
4
|
+
export { payuAdapter } from './adapters/payu/index.js';
|
|
5
|
+
export type { PayuAdapterOptions } from './adapters/payu/index.js';
|
|
6
|
+
export type { ShopConfig, ResolvedShopConfig, Currency, OrderStatus, PaymentAdapter, PaymentCreateContext, CarrierAdapter, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, I18nText } from './types.js';
|
package/dist/shop/index.js
CHANGED
|
@@ -11,7 +11,9 @@ export function defineShop(config) {
|
|
|
11
11
|
webhook: config.rateLimit?.webhook ?? { limit: 60, windowSec: 60 }
|
|
12
12
|
},
|
|
13
13
|
carriers: config.carriers ?? [],
|
|
14
|
-
consents: config.consents ?? []
|
|
14
|
+
consents: config.consents ?? [],
|
|
15
|
+
orderViewUrl: config.orderViewUrl ?? '/shop/order/{orderNumber}?token={accessToken}'
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
export { manualAdapter } from './adapters/manual/index.js';
|
|
19
|
+
export { payuAdapter } from './adapters/payu/index.js';
|
package/dist/shop/pricing.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export declare function netPlnFromGrossPln(grossPln: number, vatRate: number): number;
|
|
2
|
+
export declare function grossPlnFromNetPln(netPln: number, vatRate: number): number;
|
|
3
|
+
export declare function toCents(pln: number): number;
|
|
4
|
+
export declare function fromCents(cents: number): number;
|
|
1
5
|
export declare function grossFromNet(net: number, vatRate: number): number;
|
|
2
6
|
export declare function netFromGross(gross: number, vatRate: number): number;
|
|
3
7
|
export declare function vatAmount(net: number, vatRate: number): number;
|
package/dist/shop/pricing.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
// Pricing — dwie domeny:
|
|
2
|
+
// PLN-precise (number, do 6dp) — używane dla basePrice/priceDelta/shipping.price w DB (numeric 20,6).
|
|
3
|
+
// cents (integer, grosze) — używane w snapshotach zamówienia/faktury (KSeF).
|
|
4
|
+
// Round do groszy odbywa się RAZ, na granicy PLN→cents, po kompletnym mnożeniu netto×(1+VAT).
|
|
5
|
+
export function netPlnFromGrossPln(grossPln, vatRate) {
|
|
6
|
+
return grossPln / (1 + vatRate / 100);
|
|
7
|
+
}
|
|
8
|
+
export function grossPlnFromNetPln(netPln, vatRate) {
|
|
9
|
+
return netPln * (1 + vatRate / 100);
|
|
10
|
+
}
|
|
11
|
+
export function toCents(pln) {
|
|
12
|
+
return Math.round(pln * 100);
|
|
13
|
+
}
|
|
14
|
+
export function fromCents(cents) {
|
|
15
|
+
return cents / 100;
|
|
16
|
+
}
|
|
17
|
+
// Compat shims — `net`/`gross` w CENTACH. Deprecated; nie używać w nowych ścieżkach.
|
|
18
|
+
// Cierpią na drift dla input-as-gross + round-trip. Zostawione dla testów i zewnętrznych konsumentów.
|
|
1
19
|
export function grossFromNet(net, vatRate) {
|
|
2
20
|
return Math.round(net * (1 + vatRate / 100));
|
|
3
21
|
}
|
|
@@ -4,7 +4,7 @@ import { entriesTable } from '../../db-postgres/schema/entry.js';
|
|
|
4
4
|
import { entryVersionsTable } from '../../db-postgres/schema/entryVersion.js';
|
|
5
5
|
import { getCMS } from '../../core/cms.js';
|
|
6
6
|
import { getShopDb, requireShopConfig } from './db.js';
|
|
7
|
-
import {
|
|
7
|
+
import { grossPlnFromNetPln, toCents } from '../pricing.js';
|
|
8
8
|
export async function hydrateCart(items, opts = {}) {
|
|
9
9
|
const shop = requireShopConfig();
|
|
10
10
|
const cms = getCMS();
|
|
@@ -94,8 +94,11 @@ export async function hydrateCart(items, opts = {}) {
|
|
|
94
94
|
const version = publishedByEntry.get(product.entryId);
|
|
95
95
|
const title = readTitle(version?.data);
|
|
96
96
|
const slug = readSlug(version?.data);
|
|
97
|
-
|
|
98
|
-
const
|
|
97
|
+
// basePrice/priceDelta są numeric(20,6) → drizzle zwraca string. Konwertujemy do PLN (number).
|
|
98
|
+
const priceNetPln = Number(product.basePrice) + Number(variant.priceDelta ?? 0);
|
|
99
|
+
const priceGrossPln = grossPlnFromNetPln(priceNetPln, product.vatRate);
|
|
100
|
+
const priceNet = toCents(priceNetPln);
|
|
101
|
+
const priceGross = toCents(priceGrossPln);
|
|
99
102
|
const stock = variant.stock;
|
|
100
103
|
const stockEnabled = shop.features.stock;
|
|
101
104
|
let effectiveQty = ref.qty;
|
|
@@ -2,6 +2,7 @@ import { getCMS } from '../../core/cms.js';
|
|
|
2
2
|
import { resolveI18n } from '../pricing.js';
|
|
3
3
|
import { getOrderById, getOrderItems } from './orders.js';
|
|
4
4
|
import { requireShopConfig } from './db.js';
|
|
5
|
+
import { buildOrderViewUrl } from './order-access-url.js';
|
|
5
6
|
function formatPrice(cents, currency) {
|
|
6
7
|
return new Intl.NumberFormat('pl-PL', {
|
|
7
8
|
style: 'currency',
|
|
@@ -53,6 +54,10 @@ const STATUS_INTRO = {
|
|
|
53
54
|
en: 'Payment was not received. Please contact us if you have questions.'
|
|
54
55
|
}
|
|
55
56
|
};
|
|
57
|
+
const VIEW_LINK_LABEL = {
|
|
58
|
+
pl: 'Zobacz zamówienie',
|
|
59
|
+
en: 'View order'
|
|
60
|
+
};
|
|
56
61
|
function renderHtml(ctx, intro) {
|
|
57
62
|
const itemsRows = ctx.items
|
|
58
63
|
.map((i) => `<tr><td style="padding:6px 8px;border-bottom:1px solid #eee;">${escapeHtml(i.name)}</td><td style="padding:6px 8px;border-bottom:1px solid #eee;text-align:center;">${i.qty}</td><td style="padding:6px 8px;border-bottom:1px solid #eee;text-align:right;">${i.lineGross}</td></tr>`)
|
|
@@ -72,6 +77,9 @@ function renderHtml(ctx, intro) {
|
|
|
72
77
|
<div style="font-size:16px;">Razem (brutto): <strong style="color:#5B4A9E;">${ctx.order.totalGross}</strong></div>
|
|
73
78
|
<div style="color:#8888A0;font-size:12px;">netto ${ctx.order.totalNet} · VAT ${ctx.order.vatAmount}</div>
|
|
74
79
|
</div>
|
|
80
|
+
${ctx.viewUrl
|
|
81
|
+
? `<div style="margin-top:24px;text-align:center;"><a href="${escapeHtml(ctx.viewUrl)}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">${escapeHtml(ctx.viewLinkLabel)}</a></div>`
|
|
82
|
+
: ''}
|
|
75
83
|
</div>
|
|
76
84
|
<p style="text-align:center;color:#8888A0;font-size:12px;margin-top:16px;">AriaCMS · ${escapeHtml(ctx.order.customerEmail)}</p>
|
|
77
85
|
</div>
|
|
@@ -99,7 +107,17 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
99
107
|
const items = await getOrderItems(orderId);
|
|
100
108
|
const lang = (order.language || cms.languages[0] || 'pl');
|
|
101
109
|
const subjectKey = (lang in STATUS_SUBJECTS[status] ? lang : 'pl');
|
|
110
|
+
const viewUrl = /^https?:\/\//i.test(shop.orderViewUrl)
|
|
111
|
+
? buildOrderViewUrl(shop.orderViewUrl, {
|
|
112
|
+
orderNumber: order.number,
|
|
113
|
+
orderId: order.id,
|
|
114
|
+
accessToken: order.accessToken,
|
|
115
|
+
language: order.language
|
|
116
|
+
})
|
|
117
|
+
: null;
|
|
102
118
|
const ctx = {
|
|
119
|
+
viewUrl,
|
|
120
|
+
viewLinkLabel: VIEW_LINK_LABEL[subjectKey],
|
|
103
121
|
order: {
|
|
104
122
|
number: order.number,
|
|
105
123
|
status: order.status,
|
|
@@ -133,6 +151,4 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
133
151
|
console.error('[shop] Failed to send status email:', err);
|
|
134
152
|
// Swallow — don't block status change on email failures
|
|
135
153
|
}
|
|
136
|
-
// Silence unused for now
|
|
137
|
-
void shop;
|
|
138
154
|
}
|
|
@@ -32,6 +32,7 @@ export declare function updateOrderStatus(orderId: string, status: OrderStatus,
|
|
|
32
32
|
note?: string;
|
|
33
33
|
changedBy?: string;
|
|
34
34
|
}): Promise<OrderRow>;
|
|
35
|
+
export declare function setPaymentProviderRef(orderId: string, ref: string | null): Promise<void>;
|
|
35
36
|
export declare function getOrderById(id: string): Promise<OrderRow | null>;
|
|
36
37
|
export declare function getOrderByNumber(number: string): Promise<OrderRow | null>;
|
|
37
38
|
export declare function getOrderItems(orderId: string): Promise<OrderItemRow[]>;
|