@revenexx/cover 0.1.0 → 0.1.1
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/app.config.ts +1 -1
- package/app/components/account/address/AccountAddressCard.vue +69 -88
- package/app/components/auth/AuthRegisterPanel.vue +122 -194
- package/app/formkit.config.ts +21 -0
- package/app/interfaces/auth.ts +0 -2
- package/app/interfaces/validation.ts +1 -1
- package/app/validations/formValidationConfig.ts +0 -30
- package/nuxt.config.ts +0 -3
- package/package.json +2 -2
- package/server/api/account/orders.get.ts +6 -5
- package/server/api/account/profile.get.ts +3 -3
- package/server/api/account/profile.put.ts +6 -4
- package/server/api/auth/login.post.ts +2 -5
- package/server/api/auth/recovery.post.ts +5 -13
- package/server/api/auth/recovery.put.ts +5 -14
- package/server/api/auth/register.post.ts +21 -44
- package/server/api/orders/index.post.ts +28 -24
- package/server/api/payment/methods.post.ts +5 -4
- package/server/api/shipping/rates.post.ts +6 -4
- package/server/interfaces/auth.ts +1 -1
- package/server/services/ApiAccountService.ts +44 -0
- package/server/services/ApiAuthService.ts +15 -14
- package/server/services/ApiCartService.ts +45 -27
- package/server/services/ApiMarketService.ts +3 -5
- package/server/utils/accountService.ts +4 -5
- package/server/utils/authService.ts +0 -3
- package/server/utils/liveCatalog.ts +22 -13
- package/server/utils/liveInventories.ts +4 -4
- package/server/utils/liveOrders.ts +5 -3
- package/server/utils/livePrices.ts +10 -9
- package/server/utils/revenexxSdk.ts +162 -0
- package/app/validations/companyName.ts +0 -18
- package/app/validations/maxLength.ts +0 -12
- package/app/validations/optionalCompanyName.ts +0 -23
- package/app/validations/phoneNumber.ts +0 -19
- package/app/validations/termsRequired.ts +0 -4
- package/app/validations/zipCode.ts +0 -15
- package/server/services/SdkAccountService.ts +0 -56
- package/server/services/SdkAuthService.ts +0 -83
- package/server/utils/revenexxApi.ts +0 -136
- package/server/utils/shopSdk.ts +0 -88
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CrossFieldValidator, SimpleValidator } from "../validations/types";
|
|
2
2
|
|
|
3
|
-
export type FormKey = "login" | "
|
|
3
|
+
export type FormKey = "login" | "forgotPassword" | "resetPassword";
|
|
4
4
|
|
|
5
5
|
export interface FieldValidationRule {
|
|
6
6
|
readonly kind: "simple" | "cross";
|
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
import type { FieldConfig, FormKey } from "../interfaces/validation";
|
|
2
2
|
|
|
3
|
-
import { companyName } from "./companyName";
|
|
4
3
|
import { emailFormat } from "./emailFormat";
|
|
5
|
-
import { maxLength } from "./maxLength";
|
|
6
4
|
import { minLength } from "./minLength";
|
|
7
|
-
import { optionalCompanyName } from "./optionalCompanyName";
|
|
8
5
|
import { passwordsMatch } from "./passwordsMatch";
|
|
9
|
-
import { phoneNumber } from "./phoneNumber";
|
|
10
6
|
import { required } from "./required";
|
|
11
|
-
import { termsRequired } from "./termsRequired";
|
|
12
|
-
import { zipCode } from "./zipCode";
|
|
13
7
|
|
|
14
8
|
export const formValidationConfig: Record<FormKey, FieldConfig[]> = {
|
|
15
9
|
login: [
|
|
16
10
|
{ field: "email", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: emailFormat }] },
|
|
17
11
|
{ field: "password", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 8 } }] },
|
|
18
12
|
],
|
|
19
|
-
register: [
|
|
20
|
-
{ field: "email", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: emailFormat }] },
|
|
21
|
-
{ field: "password", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 8 } }] },
|
|
22
|
-
{ field: "passwordConfirm", rules: [{ kind: "cross", fn: passwordsMatch }] },
|
|
23
|
-
{ field: "firstName", rules: [{ kind: "simple", fn: required }] },
|
|
24
|
-
{ field: "lastName", rules: [{ kind: "simple", fn: required }] },
|
|
25
|
-
{ field: "company", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: companyName }] },
|
|
26
|
-
{ field: "acceptTerms", rules: [{ kind: "simple", fn: termsRequired }] },
|
|
27
|
-
],
|
|
28
13
|
forgotPassword: [
|
|
29
14
|
{ field: "email", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: emailFormat }] },
|
|
30
15
|
],
|
|
@@ -32,19 +17,4 @@ export const formValidationConfig: Record<FormKey, FieldConfig[]> = {
|
|
|
32
17
|
{ field: "password", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 8 } }] },
|
|
33
18
|
{ field: "passwordConfirm", rules: [{ kind: "cross", fn: passwordsMatch }] },
|
|
34
19
|
],
|
|
35
|
-
accountProfile: [
|
|
36
|
-
{ field: "phone", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: maxLength, options: { max: 16 } }, { kind: "simple", fn: phoneNumber }, { kind: "simple", fn: minLength, options: { min: 8 } }] },
|
|
37
|
-
{ field: "company", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: maxLength, options: { max: 64 } }, { kind: "simple", fn: minLength, options: { min: 3 } }, { kind: "simple", fn: companyName }] },
|
|
38
|
-
],
|
|
39
|
-
accountAddress: [
|
|
40
|
-
{ field: "label", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 2 } }] },
|
|
41
|
-
{ field: "firstName", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 2 } }] },
|
|
42
|
-
{ field: "lastName", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 2 } }] },
|
|
43
|
-
{ field: "street", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 2 } }] },
|
|
44
|
-
{ field: "streetNumber", rules: [{ kind: "simple", fn: required }] },
|
|
45
|
-
{ field: "city", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: minLength, options: { min: 2 } }] },
|
|
46
|
-
{ field: "zip", rules: [{ kind: "simple", fn: required }, { kind: "simple", fn: zipCode }] },
|
|
47
|
-
{ field: "country", rules: [{ kind: "simple", fn: required }] },
|
|
48
|
-
{ field: "company", rules: [{ kind: "simple", fn: optionalCompanyName, options: { min: 3, max: 64 } }] },
|
|
49
|
-
],
|
|
50
20
|
};
|
package/nuxt.config.ts
CHANGED
|
@@ -62,9 +62,6 @@ export default defineNuxtConfig({
|
|
|
62
62
|
// Defaults for the BFF's live backends — consuming apps override these
|
|
63
63
|
// via NUXT_* environment variables (web SDK identity, Typesense search).
|
|
64
64
|
runtimeConfig: {
|
|
65
|
-
webSdkApiUrl: "",
|
|
66
|
-
webSdkProject: "",
|
|
67
|
-
webSdkDevKey: "",
|
|
68
65
|
// Public revenexx API (gateway) — the live "api" service registry
|
|
69
66
|
// implementations. Stand-in for the generated web SDKs; the key is a
|
|
70
67
|
// tenant API key and stays server-side only.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/cover",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@formkit/vue": "^1.6.0",
|
|
39
39
|
"@iconify-json/lucide": "^1.2.111",
|
|
40
40
|
"@internationalized/date": "^3.12.0",
|
|
41
|
-
"@revenexx/sdk": "^0.0.
|
|
41
|
+
"@revenexx/sdk": "^0.0.5",
|
|
42
42
|
"@vueuse/core": "^14.3.0",
|
|
43
43
|
"typesense": "^3.0.5"
|
|
44
44
|
},
|
|
@@ -10,15 +10,16 @@ export default defineEventHandler(async (event): Promise<AccountOrderListRespons
|
|
|
10
10
|
return { orders: [] };
|
|
11
11
|
}
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const sdk = useRevenexxSdk();
|
|
14
|
+
// Raw call: orders.list does not declare its query parameters in
|
|
15
|
+
// the contract yet, so ordersList() cannot filter by contact.
|
|
16
|
+
const { items } = await sdk.call<{ items: LiveOrder[] }>("GET", "/v1/orders", {
|
|
17
|
+
query: { contact_id: refs.contact_id, limit: LIVE_HISTORY_LIMIT },
|
|
17
18
|
});
|
|
18
19
|
// The list rows carry no positions — load the aggregates (the
|
|
19
20
|
// history is capped, so this stays a bounded fan-out).
|
|
20
21
|
const aggregates = await Promise.all(
|
|
21
|
-
items.map(row =>
|
|
22
|
+
items.map(async row => await sdk.orders.ordersGet({ id: row.id }) as unknown as LiveOrder),
|
|
22
23
|
);
|
|
23
24
|
const orders = aggregates
|
|
24
25
|
.sort((a, b) => (b.placed_at ?? b.created_at).localeCompare(a.placed_at ?? a.created_at))
|
|
@@ -33,13 +33,13 @@ export default defineEventHandler(async (event): Promise<AccountProfileResponse>
|
|
|
33
33
|
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
34
34
|
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
35
35
|
}
|
|
36
|
-
if (!session.
|
|
36
|
+
if (!session.personaId && !session.userId) {
|
|
37
37
|
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
38
38
|
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Identity comes from the registry: "mock" serves the demo user, "
|
|
42
|
-
// resolves the real
|
|
41
|
+
// Identity comes from the registry: "mock" serves the demo user, "api"
|
|
42
|
+
// resolves the real customer through the public API.
|
|
43
43
|
const user = await getAccountService(event).getUser(event);
|
|
44
44
|
|
|
45
45
|
// Profile extras (phone, company) live in the mutable demo store until a
|
|
@@ -73,15 +73,17 @@ export default defineEventHandler(async (event): Promise<AccountProfileResponse>
|
|
|
73
73
|
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
if (!session.
|
|
76
|
+
if (!session.personaId && !session.userId) {
|
|
77
77
|
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
78
78
|
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// Identity comes from the registry: "mock" serves the demo user, "api"
|
|
82
|
+
// resolves the real customer through the public API.
|
|
83
|
+
let user: { $id: string | number; name: string; email: string };
|
|
82
84
|
try {
|
|
83
|
-
const accountUser = await
|
|
84
|
-
user = { $id: accountUser
|
|
85
|
+
const accountUser = await getAccountService(event).getUser(event);
|
|
86
|
+
user = { $id: accountUser.id, name: accountUser.name, email: accountUser.email };
|
|
85
87
|
}
|
|
86
88
|
catch {
|
|
87
89
|
deleteCookie(event, SESSION_COOKIE_NAME, sessionCookieOptions());
|
|
@@ -22,16 +22,13 @@ export default defineEventHandler(async (event) => {
|
|
|
22
22
|
return result;
|
|
23
23
|
}
|
|
24
24
|
catch (err) {
|
|
25
|
-
if (isSdkUserError(err)) {
|
|
26
|
-
throw createError({ statusCode: err.code, data: { type: err.type } });
|
|
27
|
-
}
|
|
28
25
|
if (isApiUserError(err)) {
|
|
29
|
-
throw createError({ statusCode: err.
|
|
26
|
+
throw createError({ statusCode: err.code, data: { type: apiAuthErrorType(err) } });
|
|
30
27
|
}
|
|
31
28
|
if (isError(err)) {
|
|
32
29
|
throw err;
|
|
33
30
|
}
|
|
34
|
-
getLogService().error("Auth request failed: login",
|
|
31
|
+
getLogService().error("Auth request failed: login", apiErrorContext(err));
|
|
35
32
|
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
36
33
|
}
|
|
37
34
|
});
|
|
@@ -6,27 +6,19 @@ export default defineEventHandler(async (event) => {
|
|
|
6
6
|
// what the reset-password page expects.
|
|
7
7
|
if (resolveAuthServiceKey(event) === "api") {
|
|
8
8
|
try {
|
|
9
|
-
await
|
|
9
|
+
await useRevenexxSdk().customers.customersAuthRecovery({ email, url });
|
|
10
10
|
return { ok: true };
|
|
11
11
|
}
|
|
12
12
|
catch (err) {
|
|
13
13
|
if (isApiUserError(err)) {
|
|
14
|
-
throw createError({ statusCode: err.
|
|
14
|
+
throw createError({ statusCode: err.code, data: { type: apiAuthErrorType(err) } });
|
|
15
15
|
}
|
|
16
16
|
getLogService().error("API request failed: recovery", apiErrorContext(err));
|
|
17
17
|
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
catch (err) {
|
|
26
|
-
if (isSdkUserError(err)) {
|
|
27
|
-
throw createError({ statusCode: err.code, data: { type: err.type } });
|
|
28
|
-
}
|
|
29
|
-
getLogService().error("SDK request failed: recovery", sdkErrorContext(err));
|
|
30
|
-
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
31
|
-
}
|
|
21
|
+
// Mock mode: no mail infrastructure — acknowledge so the demo flow
|
|
22
|
+
// (form → success state) stays walkable offline.
|
|
23
|
+
return { ok: true };
|
|
32
24
|
});
|
|
@@ -7,8 +7,8 @@ export default defineEventHandler(async (event) => {
|
|
|
7
7
|
// Public-API mode: confirm the recovery through the customers app.
|
|
8
8
|
if (resolveAuthServiceKey(event) === "api") {
|
|
9
9
|
try {
|
|
10
|
-
await
|
|
11
|
-
|
|
10
|
+
await useRevenexxSdk().customers.customersAuthRecoveryConfirm({
|
|
11
|
+
userId,
|
|
12
12
|
secret,
|
|
13
13
|
password,
|
|
14
14
|
});
|
|
@@ -16,22 +16,13 @@ export default defineEventHandler(async (event) => {
|
|
|
16
16
|
}
|
|
17
17
|
catch (err) {
|
|
18
18
|
if (isApiUserError(err)) {
|
|
19
|
-
throw createError({ statusCode: err.
|
|
19
|
+
throw createError({ statusCode: err.code, data: { type: apiAuthErrorType(err) } });
|
|
20
20
|
}
|
|
21
21
|
getLogService().error("API request failed: recovery-confirm", apiErrorContext(err));
|
|
22
22
|
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return { ok: true };
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
if (isSdkUserError(err)) {
|
|
32
|
-
throw createError({ statusCode: err.code, data: { type: err.type } });
|
|
33
|
-
}
|
|
34
|
-
getLogService().error("SDK request failed: recovery-confirm", sdkErrorContext(err));
|
|
35
|
-
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
36
|
-
}
|
|
26
|
+
// Mock mode: accept the reset so the demo flow completes offline.
|
|
27
|
+
return { ok: true };
|
|
37
28
|
});
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ID } from "@revenexx/sdk";
|
|
2
|
-
|
|
3
1
|
interface RegistrationProfilePayload {
|
|
4
2
|
mode: "new" | "existing";
|
|
5
3
|
company: string;
|
|
@@ -68,59 +66,38 @@ export default defineEventHandler(async (event) => {
|
|
|
68
66
|
// Public-API mode: the customers app is the system of record. A company
|
|
69
67
|
// registration creates the organization (and its platform team) in the
|
|
70
68
|
// same call; the contact mirrors to a platform user behind the scenes.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const { contact, user_id: userId } = await useRevenexxApi().post<{
|
|
79
|
-
contact: { id: string; email: string };
|
|
80
|
-
user_id: string;
|
|
81
|
-
}>("/v1/customers/auth/register", {
|
|
82
|
-
email,
|
|
83
|
-
password,
|
|
84
|
-
first_name: firstName || undefined,
|
|
85
|
-
last_name: lastName || undefined,
|
|
86
|
-
organization_name: profile?.company || undefined,
|
|
87
|
-
locale: resolveLocale(event),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const user = { $id: userId, name: fullName || email, email: contact.email };
|
|
91
|
-
if (profile?.company) {
|
|
92
|
-
await persistRegistrationRequest(user, profile);
|
|
93
|
-
}
|
|
94
|
-
return user;
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
if (isApiUserError(err)) {
|
|
98
|
-
throw createError({ statusCode: err.statusCode, data: { type: apiAuthErrorType(err) } });
|
|
99
|
-
}
|
|
100
|
-
getLogService().error("API request failed: register", apiErrorContext(err));
|
|
101
|
-
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
69
|
+
// Public-API mode: the customers app is the system of record. A company
|
|
70
|
+
// registration creates the organization (and its platform team) in the
|
|
71
|
+
// same call; the contact mirrors to a platform user behind the scenes.
|
|
72
|
+
const fullName = (name ?? "").trim();
|
|
73
|
+
const splitAt = fullName.lastIndexOf(" ");
|
|
74
|
+
const firstName = splitAt > 0 ? fullName.slice(0, splitAt) : fullName;
|
|
75
|
+
const lastName = splitAt > 0 ? fullName.slice(splitAt + 1) : "";
|
|
104
76
|
|
|
105
77
|
try {
|
|
106
|
-
const
|
|
107
|
-
userId: ID.unique(),
|
|
78
|
+
const { contact, user_id: userId } = await useRevenexxSdk().customers.customersAuthRegister({
|
|
108
79
|
email,
|
|
109
80
|
password,
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
...(firstName ? { firstName } : {}),
|
|
82
|
+
...(lastName ? { lastName } : {}),
|
|
83
|
+
...(profile?.company ? { organizationName: profile.company } : {}),
|
|
84
|
+
locale: resolveLocale(event),
|
|
85
|
+
}) as unknown as {
|
|
86
|
+
contact: { id: string; email: string };
|
|
87
|
+
user_id: string;
|
|
88
|
+
};
|
|
112
89
|
|
|
90
|
+
const user = { $id: userId, name: fullName || email, email: contact.email };
|
|
113
91
|
if (profile?.company) {
|
|
114
92
|
await persistRegistrationRequest(user, profile);
|
|
115
93
|
}
|
|
116
|
-
|
|
117
|
-
return { $id: user.$id, name: user.name, email: user.email };
|
|
94
|
+
return user;
|
|
118
95
|
}
|
|
119
96
|
catch (err) {
|
|
120
|
-
if (
|
|
121
|
-
throw createError({ statusCode: err.code, data: { type: err
|
|
97
|
+
if (isApiUserError(err)) {
|
|
98
|
+
throw createError({ statusCode: err.code, data: { type: apiAuthErrorType(err) } });
|
|
122
99
|
}
|
|
123
|
-
getLogService().error("
|
|
100
|
+
getLogService().error("API request failed: register", apiErrorContext(err));
|
|
124
101
|
throw createError({ statusCode: 503, data: { type: "service_unavailable" } });
|
|
125
102
|
}
|
|
126
103
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { OrderPaymentStatus } from "@revenexx/sdk";
|
|
2
|
+
import type { Models } from "@revenexx/sdk";
|
|
3
|
+
|
|
1
4
|
// Method codes are generic — live mode validates them against the
|
|
2
5
|
// payments app; the demo-only methods keep their extra fields.
|
|
3
6
|
interface OrderPayment {
|
|
@@ -140,11 +143,11 @@ export default defineEventHandler(async (event) => {
|
|
|
140
143
|
if (liveShipping && body.deliveryMethod) {
|
|
141
144
|
const address = body.address as Record<string, unknown> | undefined;
|
|
142
145
|
try {
|
|
143
|
-
const { rates } = await
|
|
144
|
-
|
|
146
|
+
const { rates } = await useRevenexxSdk().shipping.shippingRates({
|
|
147
|
+
orderValue: calculation.totals.find(row => row.key === "subtotal")?.amount
|
|
145
148
|
?? calculation.totals.find(row => row.key === "total")?.amount ?? 0,
|
|
146
149
|
country: String(address?.country ?? ""),
|
|
147
|
-
});
|
|
150
|
+
}) as unknown as { rates: Array<{ code: string; price: number }> };
|
|
148
151
|
const rate = rates.find(r => r.code === body.deliveryMethod);
|
|
149
152
|
if (!rate) {
|
|
150
153
|
throw createError({ status: 422, message: `Delivery method '${body.deliveryMethod}' is not available for this order` });
|
|
@@ -177,13 +180,15 @@ export default defineEventHandler(async (event) => {
|
|
|
177
180
|
const totalRow = calculation.totals.find(row => row.key === "total");
|
|
178
181
|
const shippingRow = calculation.totals.find(row => row.key === "shipping");
|
|
179
182
|
try {
|
|
180
|
-
const
|
|
181
|
-
|
|
183
|
+
const refs = sessionOrderRefs(event);
|
|
184
|
+
const placed = await useRevenexxSdk().orders.ordersPlace({
|
|
185
|
+
...(refs.contact_id ? { contactId: refs.contact_id } : {}),
|
|
186
|
+
...(refs.organization_id ? { organizationId: refs.organization_id } : {}),
|
|
182
187
|
currency: calculation.currency,
|
|
183
|
-
...(body.orderNumber ? {
|
|
188
|
+
...(body.orderNumber ? { customerOrderNumber: body.orderNumber } : {}),
|
|
184
189
|
buyer: user ? { name: user.name, email: user.email } : { email: body.contactEmail ?? null },
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
billingAddress: (billing ?? null) as object,
|
|
191
|
+
shippingAddress: (address ?? null) as object,
|
|
187
192
|
payment: { method: payment.method },
|
|
188
193
|
shipping: {
|
|
189
194
|
method: body.deliveryMethod ?? "standard",
|
|
@@ -191,7 +196,7 @@ export default defineEventHandler(async (event) => {
|
|
|
191
196
|
},
|
|
192
197
|
// The order carries what the customer saw (and the payment
|
|
193
198
|
// charges): the checkout calculation's total.
|
|
194
|
-
|
|
199
|
+
grandTotal: Math.max(0, Math.round(((totalRow?.amount ?? 0) + liveShippingAdjustment) * 100) / 100),
|
|
195
200
|
items: (body.items as Array<Record<string, unknown>>).map((item, index) => ({
|
|
196
201
|
product_id: String(item.id),
|
|
197
202
|
sku: String(item.sku ?? "") || undefined,
|
|
@@ -204,19 +209,19 @@ export default defineEventHandler(async (event) => {
|
|
|
204
209
|
...(item.categorySlug ? { categorySlug: String(item.categorySlug) } : {}),
|
|
205
210
|
...(item.subcategorySlug ? { subcategorySlug: String(item.subcategorySlug) } : {}),
|
|
206
211
|
},
|
|
207
|
-
})),
|
|
208
|
-
|
|
212
|
+
})) as unknown as Models.OrderItemCreateRequest[],
|
|
213
|
+
userData: {
|
|
209
214
|
...(body.deliveryNote ? { delivery_note: body.deliveryNote } : {}),
|
|
210
215
|
...(body.orderNote ? { order_note: body.orderNote } : {}),
|
|
211
216
|
...(body.partialDelivery !== undefined ? { partial_delivery: body.partialDelivery } : {}),
|
|
212
217
|
...(body.requestedDate ? { requested_date: body.requestedDate } : {}),
|
|
213
218
|
},
|
|
214
|
-
});
|
|
219
|
+
}) as unknown as { id: string; number: string };
|
|
215
220
|
orderId = placed.number;
|
|
216
221
|
liveOrderUuid = placed.id;
|
|
217
222
|
}
|
|
218
223
|
catch (err) {
|
|
219
|
-
if (err instanceof
|
|
224
|
+
if (err instanceof RevenexxException && (err.code === 400 || err.code === 422)) {
|
|
220
225
|
throw createError({ status: 422, message: err.message });
|
|
221
226
|
}
|
|
222
227
|
getLogService().error("Order placement failed", apiErrorContext(err));
|
|
@@ -237,19 +242,18 @@ export default defineEventHandler(async (event) => {
|
|
|
237
242
|
: address;
|
|
238
243
|
const country = String(body.billingCountry ?? billing?.country ?? "");
|
|
239
244
|
try {
|
|
240
|
-
const created = await
|
|
241
|
-
|
|
245
|
+
const created = await useRevenexxSdk().payments.paymentsCreate({
|
|
246
|
+
methodCode: payment.method,
|
|
242
247
|
amount: Math.max(0, Math.round(((totalRow?.amount ?? 0) + liveShippingAdjustment) * 100) / 100),
|
|
243
248
|
currency: calculation.currency,
|
|
244
249
|
country,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
idempotency_key: body.checkoutSessionToken,
|
|
250
|
+
orderRef: orderId,
|
|
251
|
+
idempotencyKey: body.checkoutSessionToken,
|
|
248
252
|
metadata: {
|
|
249
253
|
...(payment.poNumber ? { po_number: payment.poNumber } : {}),
|
|
250
254
|
...(payment.costCenter ? { cost_center: payment.costCenter } : {}),
|
|
251
255
|
},
|
|
252
|
-
});
|
|
256
|
+
}) as unknown as { id: string; status: string; error_message?: string | null };
|
|
253
257
|
if (created.status === "failed") {
|
|
254
258
|
throw createError({ status: 402, message: created.error_message ?? "Payment was declined" });
|
|
255
259
|
}
|
|
@@ -259,7 +263,7 @@ export default defineEventHandler(async (event) => {
|
|
|
259
263
|
// A placed live order must not survive a failed payment.
|
|
260
264
|
if (liveOrderUuid) {
|
|
261
265
|
try {
|
|
262
|
-
await
|
|
266
|
+
await useRevenexxSdk().orders.ordersCancel({ id: liveOrderUuid, reason: "payment failed" });
|
|
263
267
|
}
|
|
264
268
|
catch (cancelErr) {
|
|
265
269
|
getLogService().error("Order cancel after payment failure failed", apiErrorContext(cancelErr));
|
|
@@ -268,7 +272,7 @@ export default defineEventHandler(async (event) => {
|
|
|
268
272
|
if (isError(err)) {
|
|
269
273
|
throw err;
|
|
270
274
|
}
|
|
271
|
-
if (err instanceof
|
|
275
|
+
if (err instanceof RevenexxException && (err.code === 400 || err.code === 422)) {
|
|
272
276
|
throw createError({ status: 422, message: err.message });
|
|
273
277
|
}
|
|
274
278
|
getLogService().error("Payment creation failed", apiErrorContext(err));
|
|
@@ -279,10 +283,10 @@ export default defineEventHandler(async (event) => {
|
|
|
279
283
|
// The payment dimension of the live order follows the payment outcome.
|
|
280
284
|
if (liveOrderUuid && livePaymentStatus) {
|
|
281
285
|
const mapped = livePaymentStatus === "succeeded" || livePaymentStatus === "paid"
|
|
282
|
-
?
|
|
283
|
-
: livePaymentStatus === "authorized" ?
|
|
286
|
+
? OrderPaymentStatus.Paid
|
|
287
|
+
: livePaymentStatus === "authorized" ? OrderPaymentStatus.Authorized : OrderPaymentStatus.Pending;
|
|
284
288
|
try {
|
|
285
|
-
await
|
|
289
|
+
await useRevenexxSdk().orders.ordersPaymentStatusUpdate({ id: liveOrderUuid, status: mapped });
|
|
286
290
|
}
|
|
287
291
|
catch (err) {
|
|
288
292
|
getLogService().error("Order payment-status sync failed", apiErrorContext(err));
|
|
@@ -38,10 +38,11 @@ export default defineEventHandler(async (event) => {
|
|
|
38
38
|
|
|
39
39
|
const locale: Locale = resolveLocale(event);
|
|
40
40
|
try {
|
|
41
|
-
const { methods } = await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const { methods } = await useRevenexxSdk().payments.paymentsMethodsEligible({
|
|
42
|
+
amount,
|
|
43
|
+
country,
|
|
44
|
+
currency,
|
|
45
|
+
}) as unknown as { methods: ApiEligibleMethod[] };
|
|
45
46
|
|
|
46
47
|
const options: PaymentOption[] = methods.map(m => ({
|
|
47
48
|
method: m.code,
|
|
@@ -45,10 +45,12 @@ export default defineEventHandler(async (event) => {
|
|
|
45
45
|
|
|
46
46
|
const locale: Locale = resolveLocale(event);
|
|
47
47
|
try {
|
|
48
|
-
const { rates } = await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const { rates } = await useRevenexxSdk().shipping.shippingRates({
|
|
49
|
+
orderValue: amount,
|
|
50
|
+
country,
|
|
51
|
+
...(weight !== undefined ? { weight } : {}),
|
|
52
|
+
...(quantity !== undefined ? { quantity } : {}),
|
|
53
|
+
}) as unknown as { rates: ApiShippingRate[] };
|
|
52
54
|
|
|
53
55
|
const methods: DeliveryOption[] = rates.map(rate => ({
|
|
54
56
|
method: rate.code,
|
|
@@ -21,7 +21,7 @@ export interface AuthLoginResult {
|
|
|
21
21
|
* Service contract for authentication.
|
|
22
22
|
* - "mock" signs B2B demo personas in without any external dependency
|
|
23
23
|
* (see server/config/account/personas.json)
|
|
24
|
-
* - "
|
|
24
|
+
* - "api" authenticates against the platform via the public revenexx API
|
|
25
25
|
* Both implementations manage the same session cookie, so the client-side
|
|
26
26
|
* auth flow (store, guards, middleware) is identical.
|
|
27
27
|
*/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
|
|
3
|
+
import { ApiAuthService } from "./ApiAuthService";
|
|
4
|
+
import type { AccountUser, IAccountService } from "../interfaces/account";
|
|
5
|
+
|
|
6
|
+
function createInitials(name: string): string {
|
|
7
|
+
const parts = name
|
|
8
|
+
.trim()
|
|
9
|
+
.split(/\s+/)
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
if (parts.length === 0) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
return parts
|
|
15
|
+
.slice(0, 2)
|
|
16
|
+
.map(part => part[0]?.toUpperCase() ?? "")
|
|
17
|
+
.join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Live identity via the public revenexx API (customers app): resolves
|
|
22
|
+
* the current user from the request session through the same
|
|
23
|
+
* `/v1/customers/auth/me` path the auth service uses.
|
|
24
|
+
* Register via app.config → accountService: "api".
|
|
25
|
+
*/
|
|
26
|
+
export class ApiAccountService implements IAccountService {
|
|
27
|
+
private readonly auth = new ApiAuthService();
|
|
28
|
+
|
|
29
|
+
async getUser(event?: H3Event): Promise<AccountUser> {
|
|
30
|
+
if (!event) {
|
|
31
|
+
throw createError({ statusCode: 500, statusMessage: "ApiAccountService requires the request event" });
|
|
32
|
+
}
|
|
33
|
+
const user = await this.auth.me(event);
|
|
34
|
+
if (!user) {
|
|
35
|
+
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
id: user.$id,
|
|
39
|
+
name: user.name,
|
|
40
|
+
email: user.email,
|
|
41
|
+
initials: createInitials(user.name),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -34,9 +34,9 @@ function contactName(contact: ApiContact | null, fallback: string): string {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Live authentication via the public revenexx API (customers app):
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* customersAuthLogin → platform session (incl. secret) + contact
|
|
38
|
+
* auth/me (raw call) → platform user + contact
|
|
39
|
+
* customersAuthLogout → revokes the platform session
|
|
40
40
|
*
|
|
41
41
|
* Session state lives in the same cookie as the other implementations, so
|
|
42
42
|
* the client-side auth flow (store, guards, middleware) is identical. The
|
|
@@ -44,11 +44,10 @@ function contactName(contact: ApiContact | null, fallback: string): string {
|
|
|
44
44
|
*/
|
|
45
45
|
export class ApiAuthService implements IAuthService {
|
|
46
46
|
async login(event: H3Event, email: string, password: string): Promise<AuthLoginResult> {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
47
|
+
const { session, contact } = await useRevenexxSdk().customers.customersAuthLogin({ email, password }) as unknown as {
|
|
48
|
+
session: ApiSession;
|
|
49
|
+
contact: ApiContact | null;
|
|
50
|
+
};
|
|
52
51
|
|
|
53
52
|
const storedSession: StoredSession = {
|
|
54
53
|
id: session.$id,
|
|
@@ -94,11 +93,13 @@ export class ApiAuthService implements IAuthService {
|
|
|
94
93
|
|
|
95
94
|
try {
|
|
96
95
|
// session_id makes the customers app validate the session is still
|
|
97
|
-
// alive — a logged-out/revoked session answers 401 here.
|
|
98
|
-
|
|
96
|
+
// alive — a logged-out/revoked session answers 401 here. Raw call:
|
|
97
|
+
// the AuthMeRequest contract does not declare session_id yet, so
|
|
98
|
+
// the generated customersAuthMe() would drop the session check.
|
|
99
|
+
const { user, contact } = await useRevenexxSdk().call<{
|
|
99
100
|
user: { $id: string; name?: string; email: string };
|
|
100
101
|
contact: ApiContact | null;
|
|
101
|
-
}>("/v1/customers/auth/me", { user_id: parsed.userId, session_id: parsed.id });
|
|
102
|
+
}>("POST", "/v1/customers/auth/me", { body: { user_id: parsed.userId, session_id: parsed.id } });
|
|
102
103
|
|
|
103
104
|
return {
|
|
104
105
|
$id: user.$id,
|
|
@@ -122,9 +123,9 @@ export class ApiAuthService implements IAuthService {
|
|
|
122
123
|
try {
|
|
123
124
|
const parsed = JSON.parse(raw) as StoredSession;
|
|
124
125
|
if (parsed.userId && parsed.id) {
|
|
125
|
-
await
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
await useRevenexxSdk().customers.customersAuthLogout({
|
|
127
|
+
userId: parsed.userId,
|
|
128
|
+
sessionId: parsed.id,
|
|
128
129
|
});
|
|
129
130
|
}
|
|
130
131
|
}
|