@akinon/next 1.101.0-snapshot-ZERO-3615-20250924130435 → 1.101.0-snapshot-ZERO-3648-20250925195915

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # @akinon/next
2
2
 
3
- ## 1.101.0-snapshot-ZERO-3615-20250924130435
3
+ ## 1.101.0-snapshot-ZERO-3648-20250925195915
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - 6b949cb: ZERO-3615: Refactor product data handling and improve 404 error handling
7
+ - fcbbea7: ZERO-3648: Add virtual try-on feature with localization support
8
8
 
9
9
  ## 1.100.0
10
10
 
@@ -0,0 +1,209 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ const VIRTUAL_TRY_ON_API_URL = process.env.NEXT_PUBLIC_VIRTUAL_TRY_ON_API_URL;
4
+
5
+ export async function GET(request: NextRequest) {
6
+ try {
7
+ const { searchParams } = new URL(request.url);
8
+ const endpoint = searchParams.get('endpoint');
9
+
10
+ if (endpoint === 'limited-categories') {
11
+ const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/limited-categories`;
12
+
13
+ const response = await fetch(externalUrl, {
14
+ method: 'GET',
15
+ headers: {
16
+ Accept: 'application/json',
17
+ ...(request.headers.get('authorization') && {
18
+ Authorization: request.headers.get('authorization')!
19
+ })
20
+ }
21
+ });
22
+
23
+ let responseData: any;
24
+ const responseText = await response.text();
25
+
26
+ try {
27
+ responseData = responseText ? JSON.parse(responseText) : {};
28
+ } catch (parseError) {
29
+ responseData = { category_ids: [] };
30
+ }
31
+
32
+ if (!response.ok) {
33
+ responseData = { category_ids: [] };
34
+ }
35
+
36
+ return NextResponse.json(responseData, {
37
+ status: response.ok ? response.status : 200,
38
+ headers: {
39
+ 'Access-Control-Allow-Origin': '*',
40
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
41
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
42
+ }
43
+ });
44
+ }
45
+
46
+ return NextResponse.json(
47
+ { error: 'Invalid endpoint for GET method' },
48
+ { status: 400 }
49
+ );
50
+ } catch (error) {
51
+ return NextResponse.json({ category_ids: [] }, { status: 200 });
52
+ }
53
+ }
54
+
55
+ export async function POST(request: NextRequest) {
56
+ try {
57
+ const { searchParams } = new URL(request.url);
58
+ const endpoint = searchParams.get('endpoint');
59
+
60
+ const body = await request.json();
61
+
62
+ let externalUrl: string;
63
+ if (endpoint === 'feedback') {
64
+ externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/feedback`;
65
+ } else {
66
+ externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/virtual-try-on`;
67
+ }
68
+
69
+ const httpMethod = endpoint === 'feedback' ? 'PUT' : 'POST';
70
+
71
+ const response = await fetch(externalUrl, {
72
+ method: httpMethod,
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ ...(httpMethod === 'POST' && { Accept: 'application/json' }),
76
+ ...(request.headers.get('authorization') && {
77
+ Authorization: request.headers.get('authorization')!
78
+ })
79
+ },
80
+ body: JSON.stringify(body)
81
+ });
82
+
83
+ let responseData: any;
84
+ const responseText = await response.text();
85
+
86
+ if (endpoint === 'feedback') {
87
+ try {
88
+ responseData = responseText ? JSON.parse(responseText) : {};
89
+ } catch (parseError) {
90
+ responseData = { error: 'Invalid JSON response from feedback API' };
91
+ }
92
+ } else {
93
+ try {
94
+ responseData = responseText ? JSON.parse(responseText) : {};
95
+ } catch (parseError) {
96
+ responseData = { error: 'Invalid JSON response' };
97
+ }
98
+ }
99
+
100
+ if (!response.ok && responseData.error) {
101
+ let userFriendlyMessage = responseData.error;
102
+
103
+ if (
104
+ typeof responseData.error === 'string' &&
105
+ (responseData.error.includes('duplicate key value') ||
106
+ responseData.error.includes('image_pk'))
107
+ ) {
108
+ userFriendlyMessage =
109
+ 'This image has already been processed. Please try with a different image.';
110
+ } else if (responseData.error.includes('bulk insert images')) {
111
+ userFriendlyMessage =
112
+ 'There was an issue processing your image. Please try again with a different image.';
113
+ }
114
+
115
+ return NextResponse.json(
116
+ {
117
+ ...responseData,
118
+ message: userFriendlyMessage
119
+ },
120
+ {
121
+ status: response.status,
122
+ headers: {
123
+ 'Access-Control-Allow-Origin': '*',
124
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
125
+ 'Access-Control-Allow-Headers':
126
+ 'Content-Type, Accept, Authorization'
127
+ }
128
+ }
129
+ );
130
+ }
131
+
132
+ return NextResponse.json(responseData, {
133
+ status: response.status,
134
+ headers: {
135
+ 'Access-Control-Allow-Origin': '*',
136
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
137
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
138
+ }
139
+ });
140
+ } catch (error) {
141
+ return NextResponse.json(
142
+ {
143
+ status: 'error',
144
+ message:
145
+ 'Internal server error occurred during virtual try-on processing'
146
+ },
147
+ { status: 500 }
148
+ );
149
+ }
150
+ }
151
+
152
+ export async function PUT(request: NextRequest) {
153
+ try {
154
+ const { searchParams } = new URL(request.url);
155
+ const endpoint = searchParams.get('endpoint');
156
+
157
+ if (endpoint !== 'feedback') {
158
+ return NextResponse.json(
159
+ { error: 'PUT method only supports feedback endpoint' },
160
+ { status: 400 }
161
+ );
162
+ }
163
+
164
+ const body = await request.json();
165
+
166
+ const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/feedback`;
167
+
168
+ const response = await fetch(externalUrl, {
169
+ method: 'PUT',
170
+ headers: {
171
+ 'Content-Type': 'application/json',
172
+ ...(request.headers.get('authorization') && {
173
+ Authorization: request.headers.get('authorization')!
174
+ })
175
+ },
176
+ body: JSON.stringify(body)
177
+ });
178
+
179
+ const responseData = await response.json();
180
+
181
+ return NextResponse.json(responseData, {
182
+ status: response.status,
183
+ headers: {
184
+ 'Access-Control-Allow-Origin': '*',
185
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
186
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
187
+ }
188
+ });
189
+ } catch (error) {
190
+ return NextResponse.json(
191
+ {
192
+ status: 'error',
193
+ message: 'Internal server error occurred during feedback submission'
194
+ },
195
+ { status: 500 }
196
+ );
197
+ }
198
+ }
199
+
200
+ export async function OPTIONS() {
201
+ return new NextResponse(null, {
202
+ status: 200,
203
+ headers: {
204
+ 'Access-Control-Allow-Origin': '*',
205
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
206
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
207
+ }
208
+ });
209
+ }
@@ -21,7 +21,8 @@ enum Plugin {
21
21
  Akifast = 'pz-akifast',
22
22
  MultiBasket = 'pz-multi-basket',
23
23
  SavedCard = 'pz-saved-card',
24
- FlowPayment = 'pz-flow-payment'
24
+ FlowPayment = 'pz-flow-payment',
25
+ VirtualTryOn = 'pz-virtual-try-on'
25
26
  }
26
27
 
27
28
  export enum Component {
@@ -47,7 +48,8 @@ export enum Component {
47
48
  AkifastCheckoutButton = 'CheckoutButton',
48
49
  MultiBasket = 'MultiBasket',
49
50
  SavedCard = 'SavedCardOption',
50
- FlowPayment = 'FlowPayment'
51
+ FlowPayment = 'FlowPayment',
52
+ VirtualTryOnPlugin = 'VirtualTryOnPlugin'
51
53
  }
52
54
 
53
55
  const PluginComponents = new Map([
@@ -81,7 +83,8 @@ const PluginComponents = new Map([
81
83
  ],
82
84
  [Plugin.MultiBasket, [Component.MultiBasket]],
83
85
  [Plugin.SavedCard, [Component.SavedCard]],
84
- [Plugin.FlowPayment, [Component.FlowPayment]]
86
+ [Plugin.FlowPayment, [Component.FlowPayment]],
87
+ [Plugin.VirtualTryOn, [Component.VirtualTryOnPlugin]]
85
88
  ]);
86
89
 
87
90
  const getPlugin = (component: Component) => {
@@ -148,6 +151,8 @@ export default function PluginModule({
148
151
  promise = import(`${'@akinon/pz-saved-card'}`);
149
152
  } else if (plugin === Plugin.FlowPayment) {
150
153
  promise = import(`${'@akinon/pz-flow-payment'}`);
154
+ } else if (plugin === Plugin.VirtualTryOn) {
155
+ promise = import(`${'@akinon/pz-virtual-try-on'}`);
151
156
  }
152
157
  } catch (error) {
153
158
  logger.error(error);
@@ -6,7 +6,7 @@ import { ServerVariables } from '../../utils/server-variables';
6
6
  import logger from '../../utils/log';
7
7
 
8
8
  type GetProduct = {
9
- pk: number | string;
9
+ pk: number;
10
10
  locale?: string;
11
11
  currency?: string;
12
12
  searchParams?: URLSearchParams;
@@ -23,21 +23,9 @@ const getProductDataHandler = ({
23
23
  headers
24
24
  }: GetProduct) => {
25
25
  return async function () {
26
- // Convert pk to number if it's a string and validate
27
- const numericPk = typeof pk === 'string' ? parseInt(pk, 10) : pk;
28
-
29
- if (isNaN(numericPk)) {
30
- logger.warn('Invalid product pk provided', {
31
- handler: 'getProductDataHandler',
32
- pk,
33
- numericPk
34
- });
35
- return null;
36
- }
37
-
38
26
  let url = groupProduct
39
- ? product.getGroupProductByPk(numericPk)
40
- : product.getProductByPk(numericPk);
27
+ ? product.getGroupProductByPk(pk)
28
+ : product.getProductByPk(pk);
41
29
 
42
30
  if (searchParams) {
43
31
  url +=
@@ -47,81 +35,61 @@ const getProductDataHandler = ({
47
35
  .join('&');
48
36
  }
49
37
 
50
- try {
51
- const data = await appFetch<ProductResult>({
52
- url,
53
- locale,
54
- currency,
55
- init: {
56
- headers: {
57
- Accept: 'application/json',
58
- 'Content-Type': 'application/json',
59
- ...(headers ?? {})
60
- }
38
+ const data = await appFetch<ProductResult>({
39
+ url,
40
+ locale,
41
+ currency,
42
+ init: {
43
+ headers: {
44
+ Accept: 'application/json',
45
+ 'Content-Type': 'application/json',
46
+ ...(headers ?? {})
61
47
  }
62
- });
63
-
64
- // If product data is not found, return null to trigger 404
65
- if (!data?.product?.pk) {
66
- logger.warn('Product not found', {
67
- handler: 'getProductDataHandler',
68
- pk: numericPk,
69
- hasData: !!data,
70
- hasProduct: !!data?.product
71
- });
72
- return null;
73
48
  }
49
+ });
74
50
 
75
- const categoryUrl = product.categoryUrl(data.product.pk);
51
+ const categoryUrl = product.categoryUrl(data.product.pk);
76
52
 
77
- const productCategoryData = await appFetch<ProductCategoryResult>({
78
- url: categoryUrl,
79
- locale,
80
- currency,
81
- init: {
82
- headers: {
83
- Accept: 'application/json',
84
- 'Content-Type': 'application/json'
85
- }
53
+ const productCategoryData = await appFetch<ProductCategoryResult>({
54
+ url: categoryUrl,
55
+ locale,
56
+ currency,
57
+ init: {
58
+ headers: {
59
+ Accept: 'application/json',
60
+ 'Content-Type': 'application/json'
86
61
  }
87
- });
88
-
89
- const menuItemModel = productCategoryData?.results[0]?.menuitemmodel;
90
-
91
- if (!menuItemModel) {
92
- logger.warn('menuItemModel is undefined, skipping breadcrumbData fetch', {
93
- handler: 'getProductDataHandler',
94
- pk: numericPk
95
- });
96
- return { data, breadcrumbData: undefined };
97
62
  }
63
+ });
98
64
 
99
- const breadcrumbUrl = product.breadcrumbUrl(menuItemModel);
65
+ const menuItemModel = productCategoryData?.results[0]?.menuitemmodel;
100
66
 
101
- const breadcrumbData = await appFetch<any>({
102
- url: breadcrumbUrl,
103
- locale,
104
- currency,
105
- init: {
106
- headers: {
107
- Accept: 'application/json',
108
- 'Content-Type': 'application/json'
109
- }
110
- }
111
- });
112
-
113
- return {
114
- data,
115
- breadcrumbData: breadcrumbData?.menu
116
- };
117
- } catch (error) {
118
- logger.error('Error in getProductDataHandler', {
67
+ if (!menuItemModel) {
68
+ logger.warn('menuItemModel is undefined, skipping breadcrumbData fetch', {
119
69
  handler: 'getProductDataHandler',
120
- pk: numericPk,
121
- error: error.message
70
+ pk
122
71
  });
123
- return null;
72
+ return { data, breadcrumbData: undefined };
124
73
  }
74
+
75
+ const breadcrumbUrl = product.breadcrumbUrl(menuItemModel);
76
+
77
+ const breadcrumbData = await appFetch<any>({
78
+ url: breadcrumbUrl,
79
+ locale,
80
+ currency,
81
+ init: {
82
+ headers: {
83
+ Accept: 'application/json',
84
+ 'Content-Type': 'application/json'
85
+ }
86
+ }
87
+ });
88
+
89
+ return {
90
+ data,
91
+ breadcrumbData: breadcrumbData?.menu
92
+ };
125
93
  };
126
94
  };
127
95
 
@@ -133,12 +101,9 @@ export const getProductData = async ({
133
101
  groupProduct,
134
102
  headers
135
103
  }: GetProduct) => {
136
- // Convert pk to number for cache key if it's a string
137
- const numericPkForCache = typeof pk === 'string' ? parseInt(pk, 10) : pk;
138
-
139
- const result = await Cache.wrap(
104
+ return Cache.wrap(
140
105
  CacheKey[groupProduct ? 'GroupProduct' : 'Product'](
141
- numericPkForCache,
106
+ pk,
142
107
  searchParams ?? new URLSearchParams()
143
108
  ),
144
109
  locale,
@@ -155,13 +120,4 @@ export const getProductData = async ({
155
120
  compressed: true
156
121
  }
157
122
  );
158
-
159
- // If product data is not found, throw 404 error
160
- if (!result) {
161
- const error = new Error('Product not found') as Error & { status: number };
162
- error.status = 404;
163
- throw error;
164
- }
165
-
166
- return result;
167
123
  };
@@ -214,6 +214,7 @@ const withPzDefault =
214
214
 
215
215
  req.middlewareParams = {
216
216
  commerceUrl,
217
+ found: true,
217
218
  rewrites: {}
218
219
  };
219
220
 
@@ -301,6 +302,19 @@ const withPzDefault =
301
302
  )}`;
302
303
  }
303
304
 
305
+ if (
306
+ !req.middlewareParams.found &&
307
+ Settings.customNotFoundEnabled
308
+ ) {
309
+ const pathname = url.pathname
310
+ .replace(/\/+$/, '')
311
+ .split('/');
312
+ url.pathname = url.pathname.replace(
313
+ pathname.pop(),
314
+ 'pz-not-found'
315
+ );
316
+ }
317
+
304
318
  Settings.rewrites.forEach((rewrite) => {
305
319
  url.pathname = url.pathname.replace(
306
320
  rewrite.source,
@@ -30,6 +30,7 @@ export {
30
30
  export interface PzNextRequest extends NextRequest {
31
31
  middlewareParams: {
32
32
  commerceUrl: string;
33
+ found: boolean;
33
34
  rewrites: {
34
35
  locale?: string;
35
36
  prettyUrl?: string;
@@ -123,6 +123,8 @@ const withPrettyUrl =
123
123
  return middleware(req, event);
124
124
  }
125
125
 
126
+ req.middlewareParams.found = false;
127
+
126
128
  return middleware(req, event);
127
129
  };
128
130
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "1.101.0-snapshot-ZERO-3615-20250924130435",
4
+ "version": "1.101.0-snapshot-ZERO-3648-20250925195915",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "set-cookie-parser": "2.6.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@akinon/eslint-plugin-projectzero": "1.101.0-snapshot-ZERO-3615-20250924130435",
38
+ "@akinon/eslint-plugin-projectzero": "1.101.0-snapshot-ZERO-3648-20250925195915",
39
39
  "@babel/core": "7.26.10",
40
40
  "@babel/preset-env": "7.26.9",
41
41
  "@babel/preset-typescript": "7.27.0",
package/plugins.js CHANGED
@@ -16,5 +16,6 @@ module.exports = [
16
16
  'pz-tabby-extension',
17
17
  'pz-apple-pay',
18
18
  'pz-tamara-extension',
19
- 'pz-flow-payment'
19
+ 'pz-flow-payment',
20
+ 'pz-virtual-try-on'
20
21
  ];