@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,4 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from "h3";
|
|
2
|
+
import type { Models } from "@revenexx/sdk";
|
|
2
3
|
|
|
3
4
|
import type { CartItem } from "../../app/interfaces/cart-item";
|
|
4
5
|
import type { StoredSession } from "../../app/interfaces/auth";
|
|
@@ -137,11 +138,15 @@ function toSummary(cart: ApiCart): CartSummaryRow {
|
|
|
137
138
|
*/
|
|
138
139
|
export class ApiCartService implements ICartService, ICartManager {
|
|
139
140
|
private async listActive(owner: CartOwner): Promise<ApiCart[]> {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
// Raw call: carts.list does not declare its query parameters in the
|
|
142
|
+
// contract yet, so the generated cartsList() cannot filter by owner.
|
|
143
|
+
const { items } = await useRevenexxSdk().call<{ items: ApiCart[] }>("GET", "/v1/carts", {
|
|
144
|
+
query: {
|
|
145
|
+
...ownerQuery(owner),
|
|
146
|
+
status: "active",
|
|
147
|
+
limit: 100,
|
|
148
|
+
order: "updated_at.desc",
|
|
149
|
+
},
|
|
145
150
|
});
|
|
146
151
|
return items;
|
|
147
152
|
}
|
|
@@ -152,11 +157,11 @@ export class ApiCartService implements ICartService, ICartManager {
|
|
|
152
157
|
if (current) {
|
|
153
158
|
return current;
|
|
154
159
|
}
|
|
155
|
-
return
|
|
160
|
+
return await useRevenexxSdk().carts.cartsCreate({
|
|
156
161
|
name: DEFAULT_CART_NAME,
|
|
157
|
-
...(owner.contactId ? {
|
|
158
|
-
|
|
159
|
-
});
|
|
162
|
+
...(owner.contactId ? { contactId: owner.contactId } : { sessionKey: owner.sessionKey }),
|
|
163
|
+
isCurrent: true,
|
|
164
|
+
}) as unknown as ApiCart;
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
// ---- ICartService (the active cart) --------------------------------
|
|
@@ -168,16 +173,16 @@ export class ApiCartService implements ICartService, ICartManager {
|
|
|
168
173
|
if (!current) {
|
|
169
174
|
return [];
|
|
170
175
|
}
|
|
171
|
-
const { items } = await
|
|
176
|
+
const { items } = await useRevenexxSdk().carts.cartsItemsList({ cartId: current.id }) as unknown as { items: ApiCartLine[] };
|
|
172
177
|
return items.map(toCartItem);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
async saveItems(event: H3Event, items: CartItem[]): Promise<void> {
|
|
176
181
|
const owner = resolveOwner(event);
|
|
177
182
|
const cart = await this.ensureActiveCart(owner);
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
await useRevenexxSdk().carts.cartsItemsReplace({
|
|
184
|
+
cartId: cart.id,
|
|
185
|
+
items: items.map((item, index) => toApiLine(item, index)) as unknown as Models.CartItemCreateRequest[],
|
|
181
186
|
});
|
|
182
187
|
}
|
|
183
188
|
|
|
@@ -199,30 +204,43 @@ export class ApiCartService implements ICartService, ICartManager {
|
|
|
199
204
|
|
|
200
205
|
async createCart(event: H3Event, name: string, activate: boolean): Promise<CartSummaryRow> {
|
|
201
206
|
const owner = resolveOwner(event);
|
|
202
|
-
const cart = await
|
|
207
|
+
const cart = await useRevenexxSdk().carts.cartsCreate({
|
|
203
208
|
name: name.trim() || DEFAULT_CART_NAME,
|
|
204
|
-
...(owner.contactId ? {
|
|
205
|
-
|
|
206
|
-
});
|
|
209
|
+
...(owner.contactId ? { contactId: owner.contactId } : { sessionKey: owner.sessionKey }),
|
|
210
|
+
isCurrent: activate,
|
|
211
|
+
}) as unknown as ApiCart;
|
|
207
212
|
return { ...toSummary(cart), active: activate };
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
async renameCart(event: H3Event, id: string, name: string): Promise<void> {
|
|
211
|
-
await
|
|
216
|
+
await useRevenexxSdk().carts.cartsUpdate({ id, name: name.trim() });
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
async deleteCart(event: H3Event, id: string): Promise<void> {
|
|
215
|
-
await
|
|
220
|
+
await useRevenexxSdk().carts.cartsDelete({ id });
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
async activateCart(event: H3Event, id: string): Promise<CartItem[]> {
|
|
219
|
-
await
|
|
220
|
-
const { items } = await
|
|
224
|
+
await useRevenexxSdk().carts.cartsActivate({ id });
|
|
225
|
+
const { items } = await useRevenexxSdk().carts.cartsItemsList({ cartId: id }) as unknown as { items: ApiCartLine[] };
|
|
221
226
|
return items.map(toCartItem);
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
async addItemToCart(event: H3Event, id: string, item: CartItem): Promise<void> {
|
|
225
|
-
|
|
230
|
+
const line = toApiLine(item, 0);
|
|
231
|
+
await useRevenexxSdk().carts.cartsItemsCreate({
|
|
232
|
+
cartId: id,
|
|
233
|
+
...(line.product_id ? { productId: String(line.product_id) } : {}),
|
|
234
|
+
...(line.sku ? { sku: String(line.sku) } : {}),
|
|
235
|
+
name: String(line.name),
|
|
236
|
+
quantity: Number(line.quantity),
|
|
237
|
+
...(line.unit ? { unit: String(line.unit) } : {}),
|
|
238
|
+
unitPrice: Number(line.unit_price),
|
|
239
|
+
taxRate: Number(line.tax_rate),
|
|
240
|
+
position: Number(line.position),
|
|
241
|
+
snapshot: line.snapshot as object,
|
|
242
|
+
metadata: line.metadata as object,
|
|
243
|
+
});
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
async orderActiveCart(event: H3Event, orderRef: string): Promise<string | null> {
|
|
@@ -232,7 +250,7 @@ export class ApiCartService implements ICartService, ICartManager {
|
|
|
232
250
|
if (!current) {
|
|
233
251
|
return null;
|
|
234
252
|
}
|
|
235
|
-
await
|
|
253
|
+
await useRevenexxSdk().carts.cartsOrder({ id: current.id, orderRef });
|
|
236
254
|
return current.id;
|
|
237
255
|
}
|
|
238
256
|
|
|
@@ -245,10 +263,10 @@ export class ApiCartService implements ICartService, ICartManager {
|
|
|
245
263
|
// the session cart is adopted as the customer's cart.
|
|
246
264
|
const customerCarts = await this.listActive({ contactId, sessionKey: "" });
|
|
247
265
|
const target = customerCarts.find(c => c.is_current) ?? customerCarts[0];
|
|
248
|
-
await
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
...(target ? {
|
|
266
|
+
await useRevenexxSdk().carts.cartsClaim({
|
|
267
|
+
sessionKey,
|
|
268
|
+
contactId,
|
|
269
|
+
...(target ? { targetCartId: target.id } : {}),
|
|
252
270
|
});
|
|
253
271
|
}
|
|
254
272
|
}
|
|
@@ -38,16 +38,14 @@ export class ApiMarketService implements IMarketService {
|
|
|
38
38
|
return cache.markets;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
const { items } = await
|
|
41
|
+
const sdk = useRevenexxSdk();
|
|
42
|
+
const { items } = await sdk.markets.marketsList() as unknown as ApiListPage<ApiMarketRow>;
|
|
43
43
|
const active = items
|
|
44
44
|
.filter(m => m.status === "active")
|
|
45
45
|
.sort((a, b) => a.position - b.position || a.code.localeCompare(b.code));
|
|
46
46
|
|
|
47
47
|
const markets = await Promise.all(active.map(async (market): Promise<ShopMarket> => {
|
|
48
|
-
const { items: locales } = await
|
|
49
|
-
`/v1/markets/${encodeURIComponent(market.id)}/locales`,
|
|
50
|
-
);
|
|
48
|
+
const { items: locales } = await sdk.markets.marketsLocalesList({ marketId: market.id }) as unknown as ApiListPage<ApiLocaleRow>;
|
|
51
49
|
return {
|
|
52
50
|
id: market.id,
|
|
53
51
|
code: market.code,
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { H3Event } from "h3";
|
|
2
2
|
|
|
3
3
|
import type { IAccountService } from "../interfaces/account";
|
|
4
|
+
import { ApiAccountService } from "../services/ApiAccountService";
|
|
4
5
|
import { MockAccountService } from "../services/MockAccountService";
|
|
5
|
-
import { SdkAccountService } from "../services/SdkAccountService";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Registry of available account service implementations.
|
|
9
9
|
* The active implementation is selected by the `accountService` key in app.config.ts:
|
|
10
10
|
* - "mock" — demo identity from the bundled config asset
|
|
11
|
-
* - "
|
|
12
|
-
* runtime config and an authenticated session)
|
|
11
|
+
* - "api" — live identity via the public revenexx API (customers app)
|
|
13
12
|
*/
|
|
14
13
|
const serviceRegistry: Record<string, IAccountService> = {
|
|
15
14
|
mock: new MockAccountService("account/user.json"),
|
|
16
|
-
|
|
15
|
+
api: new ApiAccountService(),
|
|
17
16
|
};
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -24,7 +23,7 @@ export function getAccountService(event?: H3Event): IAccountService {
|
|
|
24
23
|
const key = resolveServiceKey(event, {
|
|
25
24
|
domain: "accountService",
|
|
26
25
|
mockKey: "mock",
|
|
27
|
-
liveKey: "
|
|
26
|
+
liveKey: "api",
|
|
28
27
|
});
|
|
29
28
|
return serviceRegistry[key] ?? serviceRegistry["mock"]!;
|
|
30
29
|
}
|
|
@@ -3,20 +3,17 @@ import type { H3Event } from "h3";
|
|
|
3
3
|
import type { IAuthService } from "../interfaces/auth";
|
|
4
4
|
import { ApiAuthService } from "../services/ApiAuthService";
|
|
5
5
|
import { MockAuthService } from "../services/MockAuthService";
|
|
6
|
-
import { SdkAuthService } from "../services/SdkAuthService";
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Registry of available auth implementations:
|
|
10
9
|
* - "mock" — offline login as bundled B2B demo personas (buyer/approver/admin)
|
|
11
10
|
* - "api" — live authentication via the public revenexx API (customers app)
|
|
12
|
-
* - "sdk" — live authentication via the revenexx web SDK (direct platform)
|
|
13
11
|
*/
|
|
14
12
|
const mockAuthService = new MockAuthService("account/personas.json");
|
|
15
13
|
|
|
16
14
|
const serviceRegistry: Record<string, IAuthService> = {
|
|
17
15
|
mock: mockAuthService,
|
|
18
16
|
api: new ApiAuthService(),
|
|
19
|
-
sdk: new SdkAuthService(),
|
|
20
17
|
};
|
|
21
18
|
|
|
22
19
|
export function getAuthService(event?: H3Event): IAuthService {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Collection } from "@revenexx/sdk";
|
|
2
|
+
|
|
1
3
|
import type { ProductCategory, ProductSubcategory } from "../../app/config/navigation";
|
|
2
4
|
import type { Product } from "../../app/composables/useProducts";
|
|
3
5
|
import type { ProductDetail } from "../../app/interfaces/product-detail";
|
|
@@ -58,16 +60,22 @@ const LIVE_DEFAULT_TAX_RATE = 19;
|
|
|
58
60
|
const LIVE_DEFAULT_STOCK = { quantity: 9999, maxOrderQuantity: 999999 };
|
|
59
61
|
|
|
60
62
|
export async function searchLiveProducts(params: LiveSearchParams): Promise<LiveSearchResponse> {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
// Raw call (GET form): the gateway currently 404s the POST search route
|
|
64
|
+
// its spec declares, and the GET contract does not declare facet_by —
|
|
65
|
+
// the generated search methods can't cover this call yet.
|
|
66
|
+
return useRevenexxSdk().call<LiveSearchResponse>(
|
|
67
|
+
"GET",
|
|
68
|
+
`/v1/search/collections/${Collection.Products}/documents/search`,
|
|
63
69
|
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
query: {
|
|
71
|
+
q: params.q,
|
|
72
|
+
query_by: params.queryBy ?? "name,sku,description,manufacturer",
|
|
73
|
+
filter_by: params.filterBy,
|
|
74
|
+
facet_by: params.facetBy,
|
|
75
|
+
sort_by: params.sortBy,
|
|
76
|
+
page: params.page ?? 1,
|
|
77
|
+
per_page: params.perPage ?? 24,
|
|
78
|
+
},
|
|
71
79
|
},
|
|
72
80
|
);
|
|
73
81
|
}
|
|
@@ -174,14 +182,15 @@ async function loadLiveCategoryRows(): Promise<LiveCategoryRow[]> {
|
|
|
174
182
|
return categoryCache.rows;
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
const
|
|
185
|
+
const sdk = useRevenexxSdk();
|
|
178
186
|
const rows: LiveCategoryRow[] = [];
|
|
179
187
|
let offset = 0;
|
|
180
188
|
// Paginate defensively — the demo tenant has a few hundred categories.
|
|
189
|
+
// Raw call: the categories list contract does not declare limit/offset
|
|
190
|
+
// yet, so the generated productsCategoriesList() cannot paginate.
|
|
181
191
|
for (let pageIndex = 0; pageIndex < 25; pageIndex++) {
|
|
182
|
-
const page = await
|
|
183
|
-
limit: CATEGORY_PAGE_LIMIT,
|
|
184
|
-
offset,
|
|
192
|
+
const page = await sdk.call<LiveCategoryPage>("GET", "/v1/products/categories", {
|
|
193
|
+
query: { limit: CATEGORY_PAGE_LIMIT, offset },
|
|
185
194
|
});
|
|
186
195
|
rows.push(...page.items);
|
|
187
196
|
if (!page.page.hasMore) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from "h3";
|
|
2
|
+
import type { Models } from "@revenexx/sdk";
|
|
2
3
|
|
|
3
4
|
import type { Product } from "../../app/composables/useProducts";
|
|
4
5
|
import type { ProductDetail } from "../../app/interfaces/product-detail";
|
|
@@ -30,10 +31,9 @@ function toStock(entry: ItemAvailability): { quantity: number; maxOrderQuantity:
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
async function fetchAvailability(items: Array<{ product_id?: string; sku?: string }>): Promise<Map<string, ItemAvailability>> {
|
|
33
|
-
const { availability } = await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
34
|
+
const { availability } = await useRevenexxSdk().inventories.inventoriesAvailability({
|
|
35
|
+
items: items as Models.InventoryAvailabilityItem[],
|
|
36
|
+
}) as unknown as { availability: ItemAvailability[] };
|
|
37
37
|
const byKey = new Map<string, ItemAvailability>();
|
|
38
38
|
for (const entry of availability) {
|
|
39
39
|
if (entry.product_id) byKey.set(entry.product_id, entry);
|
|
@@ -129,11 +129,13 @@ export function mapLiveOrderToAccount(order: LiveOrder): AccountOrder {
|
|
|
129
129
|
|
|
130
130
|
/** Fetch one live order by its display number (the account history id). */
|
|
131
131
|
export async function getLiveOrderByNumber(number: string): Promise<LiveOrder | null> {
|
|
132
|
-
const
|
|
133
|
-
|
|
132
|
+
const sdk = useRevenexxSdk();
|
|
133
|
+
// Raw call: orders.list does not declare its query parameters in the
|
|
134
|
+
// contract yet, so the generated ordersList() cannot filter by number.
|
|
135
|
+
const { items } = await sdk.call<{ items: LiveOrder[] }>("GET", "/v1/orders", { query: { number, limit: 1 } });
|
|
134
136
|
const row = items[0];
|
|
135
137
|
if (!row) {
|
|
136
138
|
return null;
|
|
137
139
|
}
|
|
138
|
-
return
|
|
140
|
+
return await sdk.orders.ordersGet({ id: row.id }) as unknown as LiveOrder;
|
|
139
141
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { H3Event } from "h3";
|
|
2
|
+
import type { Models } from "@revenexx/sdk";
|
|
2
3
|
|
|
3
4
|
import type { StoredSession } from "../../app/interfaces/auth";
|
|
4
5
|
import type { Product } from "../../app/composables/useProducts";
|
|
@@ -21,7 +22,7 @@ interface ResolvedPrice {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/** The buyer's price scope from the session (guests resolve openly). */
|
|
24
|
-
function priceContext(event: H3Event): {
|
|
25
|
+
function priceContext(event: H3Event): { contactId?: string; organizationId?: string } {
|
|
25
26
|
const raw = getCookie(event, SESSION_COOKIE_NAME);
|
|
26
27
|
if (!raw) {
|
|
27
28
|
return {};
|
|
@@ -29,8 +30,8 @@ function priceContext(event: H3Event): { contact_id?: string; organization_id?:
|
|
|
29
30
|
try {
|
|
30
31
|
const parsed = JSON.parse(raw) as StoredSession;
|
|
31
32
|
return {
|
|
32
|
-
...(parsed.contactId ? {
|
|
33
|
-
...(parsed.organizationId ? {
|
|
33
|
+
...(parsed.contactId ? { contactId: parsed.contactId } : {}),
|
|
34
|
+
...(parsed.organizationId ? { organizationId: parsed.organizationId } : {}),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
catch {
|
|
@@ -61,10 +62,10 @@ export async function enrichProductsWithLivePrices(event: H3Event, products: Pro
|
|
|
61
62
|
if (products.length === 0) {
|
|
62
63
|
return products;
|
|
63
64
|
}
|
|
64
|
-
const { prices } = await
|
|
65
|
-
items: products.map(p => ({ product_id: p.id, sku: p.sku, quantity: 1 })),
|
|
65
|
+
const { prices } = await useRevenexxSdk().prices.pricesResolve({
|
|
66
|
+
items: products.map(p => ({ product_id: p.id, sku: p.sku, quantity: 1 })) as Models.PriceResolveItem[],
|
|
66
67
|
...priceContext(event),
|
|
67
|
-
});
|
|
68
|
+
}) as unknown as { prices: ResolvedPrice[] };
|
|
68
69
|
|
|
69
70
|
const byKey = new Map<string, ResolvedPrice>();
|
|
70
71
|
for (const price of prices) {
|
|
@@ -84,10 +85,10 @@ export async function enrichDetailWithLivePrices(event: H3Event, detail: Product
|
|
|
84
85
|
if (!product) {
|
|
85
86
|
return detail;
|
|
86
87
|
}
|
|
87
|
-
const { prices } = await
|
|
88
|
-
items: [{ product_id: product.id, sku: product.sku, quantity: 1 }],
|
|
88
|
+
const { prices } = await useRevenexxSdk().prices.pricesResolve({
|
|
89
|
+
items: [{ product_id: product.id, sku: product.sku, quantity: 1 }] as Models.PriceResolveItem[],
|
|
89
90
|
...priceContext(event),
|
|
90
|
-
});
|
|
91
|
+
}) as unknown as { prices: ResolvedPrice[] };
|
|
91
92
|
const resolved = prices[0];
|
|
92
93
|
return { ...detail, prices: resolved ? toPriceMap(resolved) : [] };
|
|
93
94
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Carts,
|
|
3
|
+
Client,
|
|
4
|
+
Customers,
|
|
5
|
+
Inventories,
|
|
6
|
+
Markets,
|
|
7
|
+
Orders,
|
|
8
|
+
Payments,
|
|
9
|
+
Prices,
|
|
10
|
+
Products,
|
|
11
|
+
RevenexxException,
|
|
12
|
+
Search,
|
|
13
|
+
Shipping,
|
|
14
|
+
} from "@revenexx/sdk";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The generated revenexx web SDK, configured for the BFF. Every live
|
|
18
|
+
* "api"-keyed implementation goes through these service instances.
|
|
19
|
+
*
|
|
20
|
+
* Auth model: tenant API key (server-side only, never reaches the client).
|
|
21
|
+
* Configure via runtime config / env:
|
|
22
|
+
* NUXT_REVENEXX_API_URL (default https://api.revenexx.com)
|
|
23
|
+
* NUXT_REVENEXX_TENANT (tenant slug, e.g. "revenexx")
|
|
24
|
+
* NUXT_REVENEXX_API_KEY (rvxk_… gateway key)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Re-exported so route handlers can narrow SDK failures without importing
|
|
28
|
+
// from the package themselves (Nitro auto-imports this util's exports).
|
|
29
|
+
export { RevenexxException };
|
|
30
|
+
|
|
31
|
+
export interface RevenexxSdkCallOptions {
|
|
32
|
+
readonly query?: Record<string, string | number | boolean | undefined>;
|
|
33
|
+
readonly body?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RevenexxSdk {
|
|
37
|
+
readonly client: Client;
|
|
38
|
+
readonly carts: Carts;
|
|
39
|
+
readonly customers: Customers;
|
|
40
|
+
readonly inventories: Inventories;
|
|
41
|
+
readonly markets: Markets;
|
|
42
|
+
readonly orders: Orders;
|
|
43
|
+
readonly payments: Payments;
|
|
44
|
+
readonly prices: Prices;
|
|
45
|
+
readonly products: Products;
|
|
46
|
+
readonly search: Search;
|
|
47
|
+
readonly shipping: Shipping;
|
|
48
|
+
/**
|
|
49
|
+
* Low-level escape hatch over the SDK transport (same auth, same error
|
|
50
|
+
* type) for the few operations whose gateway contracts are not complete
|
|
51
|
+
* yet — list query parameters and auth/me's session check. Each call
|
|
52
|
+
* site documents the missing contract piece; once the apps declare it
|
|
53
|
+
* and the SDK regenerates, the call moves to the typed method.
|
|
54
|
+
*/
|
|
55
|
+
call<T>(method: "GET" | "POST" | "PUT" | "DELETE", path: string, options?: RevenexxSdkCallOptions): Promise<T>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expected user-facing failures (wrong credentials, duplicate email, …) are
|
|
60
|
+
* forwarded to the client as-is; everything else is an infrastructure error.
|
|
61
|
+
*/
|
|
62
|
+
const API_USER_ERROR_STATUS = new Set([400, 401, 404, 409, 429]);
|
|
63
|
+
|
|
64
|
+
export function isApiUserError(err: unknown): err is RevenexxException {
|
|
65
|
+
return err instanceof RevenexxException && API_USER_ERROR_STATUS.has(err.code);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The customers app's auth passthrough answers with a generic error envelope
|
|
70
|
+
* (no machine type). Map the HTTP status onto the platform error types the
|
|
71
|
+
* client-side auth store already translates, so mock/api modes produce
|
|
72
|
+
* identical UX for the standard failures.
|
|
73
|
+
*/
|
|
74
|
+
export function apiAuthErrorType(err: RevenexxException): string {
|
|
75
|
+
switch (err.code) {
|
|
76
|
+
case 401: return "user_invalid_credentials";
|
|
77
|
+
case 404: return "user_not_found";
|
|
78
|
+
case 409: return "user_email_already_exists";
|
|
79
|
+
case 429: return "general_rate_limit_exceeded";
|
|
80
|
+
default: return err.type || "api_error";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Diagnostic fields for structured logs — never includes the API key. */
|
|
85
|
+
export function apiErrorContext(err: unknown): Record<string, unknown> {
|
|
86
|
+
if (err instanceof RevenexxException) {
|
|
87
|
+
return { apiStatus: err.code, apiType: err.type, apiMessage: err.message };
|
|
88
|
+
}
|
|
89
|
+
return { errorMessage: err instanceof Error ? err.message : String(err) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let sdk: RevenexxSdk | null = null;
|
|
93
|
+
|
|
94
|
+
export function useRevenexxSdk(): RevenexxSdk {
|
|
95
|
+
if (sdk) {
|
|
96
|
+
return sdk;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const config = useRuntimeConfig();
|
|
100
|
+
const endpoint = String(config.revenexxApiUrl || "https://api.revenexx.com").replace(/\/+$/, "");
|
|
101
|
+
const tenant = String(config.revenexxTenant || "");
|
|
102
|
+
const apiKey = String(config.revenexxApiKey || "");
|
|
103
|
+
|
|
104
|
+
if (!tenant || !apiKey) {
|
|
105
|
+
throw new RevenexxException(
|
|
106
|
+
"revenexx API credentials are not configured (NUXT_REVENEXX_TENANT / NUXT_REVENEXX_API_KEY)",
|
|
107
|
+
503,
|
|
108
|
+
"api_not_configured",
|
|
109
|
+
"",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const client = new Client()
|
|
114
|
+
.setEndpoint(endpoint)
|
|
115
|
+
.setTenant(tenant)
|
|
116
|
+
.setApiKeyAuth(apiKey);
|
|
117
|
+
|
|
118
|
+
async function call<T>(
|
|
119
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
120
|
+
path: string,
|
|
121
|
+
options: RevenexxSdkCallOptions = {},
|
|
122
|
+
): Promise<T> {
|
|
123
|
+
const url = new URL(endpoint + path);
|
|
124
|
+
const query: Record<string, string> = {};
|
|
125
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
126
|
+
if (value !== undefined) {
|
|
127
|
+
query[key] = String(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (method === "GET") {
|
|
132
|
+
// client.call appends flattened params to the URL for GET requests
|
|
133
|
+
return await client.call("get", url, {}, query) as T;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const [key, value] of Object.entries(query)) {
|
|
137
|
+
url.searchParams.set(key, value);
|
|
138
|
+
}
|
|
139
|
+
return await client.call(
|
|
140
|
+
method.toLowerCase(),
|
|
141
|
+
url,
|
|
142
|
+
{ "content-type": "application/json" },
|
|
143
|
+
(options.body ?? {}) as never,
|
|
144
|
+
) as T;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sdk = {
|
|
148
|
+
client,
|
|
149
|
+
carts: new Carts(client),
|
|
150
|
+
customers: new Customers(client),
|
|
151
|
+
inventories: new Inventories(client),
|
|
152
|
+
markets: new Markets(client),
|
|
153
|
+
orders: new Orders(client),
|
|
154
|
+
payments: new Payments(client),
|
|
155
|
+
prices: new Prices(client),
|
|
156
|
+
products: new Products(client),
|
|
157
|
+
search: new Search(client),
|
|
158
|
+
shipping: new Shipping(client),
|
|
159
|
+
call,
|
|
160
|
+
};
|
|
161
|
+
return sdk;
|
|
162
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
// checks if the input is a valid company name (basic validation)
|
|
4
|
-
export const companyName: SimpleValidator = (value) => {
|
|
5
|
-
const companyNameRegex = /^[a-zA-Z0-9äöüÄÖÜß\s.,'-]+$/;
|
|
6
|
-
|
|
7
|
-
if (typeof value !== "string") {
|
|
8
|
-
return null; // Not a string, let other validators handle this
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const trimmedValue = value.trim();
|
|
12
|
-
|
|
13
|
-
if (!companyNameRegex.test(trimmedValue)) {
|
|
14
|
-
return "invalidCompanyName"; // Invalid company name format
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return null; // Valid company name
|
|
18
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
export const maxLength: SimpleValidator = (value, options) => {
|
|
4
|
-
const max = (options?.max as number) ?? Infinity;
|
|
5
|
-
if (typeof value !== "string") {
|
|
6
|
-
return null;
|
|
7
|
-
}
|
|
8
|
-
if (value.trim().length > max) {
|
|
9
|
-
return "maxLength";
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
// Validates company name only when a value is present — field is optional.
|
|
4
|
-
// Options { min, max } are forwarded to minLength/maxLength translation keys.
|
|
5
|
-
export const optionalCompanyName: SimpleValidator = (value) => {
|
|
6
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
const min = 3;
|
|
11
|
-
const max = 64;
|
|
12
|
-
if (trimmed.length < min) {
|
|
13
|
-
return "minLength";
|
|
14
|
-
}
|
|
15
|
-
if (trimmed.length > max) {
|
|
16
|
-
return "maxLength";
|
|
17
|
-
}
|
|
18
|
-
const companyNameRegex = /^[a-zA-Z0-9äöüÄÖÜß\s.,'-]+$/;
|
|
19
|
-
if (!companyNameRegex.test(trimmed)) {
|
|
20
|
-
return "invalidCompanyName";
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
// checks if the input is a valid phone number (basic validation)
|
|
4
|
-
export const phoneNumber: SimpleValidator = (value) => {
|
|
5
|
-
if (typeof value !== "string") {
|
|
6
|
-
return null; // Not a string, let other validators handle this
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const trimmedValue = value.trim();
|
|
10
|
-
|
|
11
|
-
// Basic regex for phone numbers (allows digits, spaces, dashes, parentheses, and plus sign)
|
|
12
|
-
const phoneRegex = /^[+\d]?(?:[\d\s-().]*)$/;
|
|
13
|
-
|
|
14
|
-
if (!phoneRegex.test(trimmedValue)) {
|
|
15
|
-
return "invalidPhoneNumber"; // Invalid phone number format
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return null; // Valid phone number
|
|
19
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { SimpleValidator } from "./types";
|
|
2
|
-
|
|
3
|
-
export const zipCode: SimpleValidator = (value) => {
|
|
4
|
-
if (typeof value !== "string") {
|
|
5
|
-
return "invalidZipCode";
|
|
6
|
-
}
|
|
7
|
-
const trimmed = value.trim();
|
|
8
|
-
if (!/^\d{3,5}$/.test(trimmed)) {
|
|
9
|
-
return "invalidZipCode";
|
|
10
|
-
}
|
|
11
|
-
if (/^0+$/.test(trimmed) || trimmed === "99999") {
|
|
12
|
-
return "invalidZipCode";
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
};
|