summaryrefslogtreecommitdiff
path: root/src/pages/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages/api')
-rw-r--r--src/pages/api/magento-product.ts168
-rw-r--r--src/pages/api/shop/search.js347
2 files changed, 382 insertions, 133 deletions
diff --git a/src/pages/api/magento-product.ts b/src/pages/api/magento-product.ts
new file mode 100644
index 00000000..28738878
--- /dev/null
+++ b/src/pages/api/magento-product.ts
@@ -0,0 +1,168 @@
+// pages/api/magento-product.ts
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ // Kita terima 'skus' (banyak) dan 'main_sku' (utama/pertama)
+ const { skus, main_sku } = req.query;
+
+ if (!skus) {
+ return res.status(400).json({ error: 'SKUs are required' });
+ }
+
+ const token = process.env.MAGENTO_API_KEY || '';
+ const baseUrl = process.env.MAGENTO_API_HOST || '';
+
+ try {
+ const skuList = String(skus).split(','); // Contoh: ['221', '222', '223']
+ const mainSku = String(main_sku || skuList[0]).trim(); // Fallback ke yang pertama
+
+ // =====================================================================
+ // 1. FETCH SEMUA VARIAN SEKALIGUS (Optimasi 'IN' Operator)
+ // =====================================================================
+ const searchParams = new URLSearchParams({
+ 'searchCriteria[filter_groups][0][filters][0][field]': 'sku',
+ 'searchCriteria[filter_groups][0][filters][0][value]': skuList.join(','),
+ 'searchCriteria[filter_groups][0][filters][0][condition_type]': 'in'
+ });
+
+ const productUrl = `${baseUrl}/products?${searchParams.toString()}`;
+
+ const productResponse = await fetch(productUrl, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!productResponse.ok) {
+ return res.status(200).json({ specsMatrix: [], upsell_ids: [], related_ids: [] });
+ }
+
+ const productData = await productResponse.json();
+ const items = productData.items || [];
+
+ if (items.length === 0) {
+ return res.status(200).json({ specsMatrix: [], upsell_ids: [], related_ids: [] });
+ }
+
+ const cleanAttributeValue = (val: any) => {
+ if (val === null || val === undefined) return '';
+ let str = String(val).trim();
+ if (str.length >= 2 && str.startsWith('"') && str.endsWith('"')) {
+ str = str.slice(1, -1).trim();
+ }
+ return str;
+ };
+
+ // =====================================================================
+ // 2. BUILD SPECS MATRIX
+ // =====================================================================
+
+ // Kumpulkan semua kode atribut unik
+ const allAttributeCodes = new Set<string>();
+ items.forEach((p: any) => {
+ if (p.custom_attributes) {
+ p.custom_attributes.forEach((attr: any) => {
+ if (attr.attribute_code.startsWith('z')) {
+ allAttributeCodes.add(attr.attribute_code);
+ }
+ });
+ }
+ });
+
+ // Fetch Label untuk atribut-atribut tersebut (Sekali jalan)
+ const labelsMap: Record<string, string> = {};
+ await Promise.all(Array.from(allAttributeCodes).map(async (code) => {
+ try {
+ const attrUrl = `${baseUrl}/products/attributes/${code}`;
+ const res = await fetch(attrUrl, { headers: { 'Authorization': `Bearer ${token}` } });
+ if (res.ok) {
+ const json = await res.json();
+ labelsMap[code] = json.default_frontend_label || code;
+ }
+ } catch (e) {}
+
+ // Fallback label jika gagal
+ if (!labelsMap[code]) {
+ labelsMap[code] = code.substring(1).replace(/_/g, ' ').trim();
+ }
+ }));
+
+ // Susun Matrix
+ // Struktur: { code, label, values: { [sku]: value } }
+ const matrix: any[] = [];
+
+ Array.from(allAttributeCodes).forEach((code) => {
+ const row: any = {
+ code: code,
+ label: labelsMap[code],
+ values: {}
+ };
+
+ let hasData = false;
+
+ items.forEach((p: any) => {
+ const attr = p.custom_attributes.find((a: any) => a.attribute_code === code);
+ // Gunakan helper cleanAttributeValue disini
+ const rawVal = attr ? cleanAttributeValue(attr.value) : '';
+
+ if (rawVal !== '' && rawVal !== '-') {
+ hasData = true;
+ }
+ row.values[p.sku] = rawVal;
+ });
+
+ if (hasData) {
+ matrix.push(row);
+ }
+ });
+
+ // Deskripsi produk per varian
+ const descriptions:Record<string, string> = {};
+ items.forEach((p: any) => {
+ const descAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'description' || a.attribute_code === 'short_description');
+ descriptions[p.sku] = descAttr ? descAttr.value : '';
+ });
+
+ const warranties: Record<string, string> = {};
+ items.forEach((p: any) => {
+ const warAttr = p.custom_attributes.find((a: any) => a.attribute_code === 'z_warranty');
+ warranties[p.sku] = warAttr ? cleanAttributeValue(warAttr.value) : '';
+ });
+
+ // =====================================================================
+ // 3. AMBIL LINKS (UPSELL & RELATED) DARI MAIN VARIANT SAJA
+ // =====================================================================
+ const mainProduct = items.find((p: any) => String(p.sku) === mainSku) || items[0];
+
+ let upsellIds: number[] = [];
+ let relatedIds: number[] = [];
+
+ if (mainProduct && mainProduct.product_links) {
+ mainProduct.product_links.forEach((link: any) => {
+ if (link.link_type === 'upsell') {
+ upsellIds.push(Number(link.linked_product_sku));
+ } else if (link.link_type === 'related') {
+ relatedIds.push(Number(link.linked_product_sku));
+ }
+ });
+ }
+
+ // Response
+ res.status(200).json({
+ specsMatrix: matrix,
+ upsell_ids: upsellIds,
+ related_ids: relatedIds,
+ descriptions: descriptions,
+ warranties: warranties,
+ });
+
+ } catch (error) {
+ console.error('Proxy Error:', error);
+ res.status(500).json({ error: 'Internal Server Error' });
+ }
+} \ No newline at end of file
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index 42d16100..89d75cd0 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -2,6 +2,14 @@ import { productMappingSolr } from '@/utils/solrMapping';
import axios from 'axios';
import camelcaseObjectDeep from 'camelcase-object-deep';
+const escapeSolrQuery = (query) => {
+ if (query == '*') return query;
+ query = query.replace(/-/g, ' ');
+ const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
+ return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' ');
+};
+
export default async function handler(req, res) {
const {
q = '*',
@@ -12,115 +20,230 @@ export default async function handler(req, res) {
priceTo = 0,
orderBy = '',
operation = 'AND',
- fq = '',
+ fq = '', // bisa berupa string atau array
limit = 30,
source = '',
+ group = 'true',
} = req.query;
let { stock = '' } = req.query;
// ============================================================
- // SITEMAP
+ // [PERBAIKAN] 1. LOGIC KHUSUS COMPARE (PAKAI URLSearchParams)
// ============================================================
- if (source === 'sitemap') {
+ if (source === 'compare') {
try {
- const offset = (page - 1) * limit;
+ let qCompare = q === '*' ? '*:*' : q;
+
+ if (qCompare !== '*:*') {
+ qCompare = escapeSolrQuery(qCompare);
+ qCompare = qCompare.split(/\s+/).map(term => {
+ if (term && !term.includes('*')) {
+ return term + '*';
+ }
+ return term;
+ }).join(' ');
+ }
+
+ // [SOLUSI] Gunakan URLSearchParams untuk menyusun URL dengan aman
+ const params = new URLSearchParams();
+
+ params.append('q', qCompare);
+ params.append('rows', limit);
+ params.append('wt', 'json');
+ params.append('indent', 'true');
+
+ // Gunakan eDisMax parser (Otak Cerdas)
+ params.append('defType', 'edismax');
+
+ // Set Prioritas Pencarian (Boost ^)
+ // 1. default_code_s^20 : SKU persis (Prioritas Tertinggi)
+ // 2. search_keywords_t^10 : Field baru (Case insensitive)
+ // 3. display_name_s^1 : Cadangan
+ params.append('qf', 'default_code_s^20 search_keywords_t^10 display_name_s^1');
+
+ const compareWords = qCompare.split(/\s+/).filter(w => w.length > 0);
+ let compareMm = '100%';
+ if (compareWords.length >= 3) {
+ compareMm = '75%';
+ }
+ params.append('mm', compareMm);
+
+ if (group === 'false') {
+ params.append('group', 'false');
+ } else {
+ params.append('group', 'true');
+ params.append('group.field', 'template_id_i');
+ params.append('group.limit', '1');
+ params.append('group.main', 'true');
+ }
+
+ // Field List (fl)
+ params.append('fl', 'id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i');
+
+ // Filter Query (fq) Dasar
+ params.append('fq', '-publish_b:false');
+ params.append('fq', 'price_tier1_v2_f:[1 TO *]');
+
+ // Logic Locking (Filter Attribute Set ID dari Frontend)
+ if (fq) {
+ if (Array.isArray(fq)) {
+ fq.forEach(f => params.append('fq', f));
+ } else {
+ params.append('fq', fq);
+ }
+ }
+
+ // Target Core: VARIANTS
+ // HAPUS parameter manual dari string URL, gunakan params object
+ const solrUrl = process.env.SOLR_HOST + '/solr/variants/select';
+
+ // Axios akan otomatis handle encoding % dan & dengan benar
+ const result = await axios.get(solrUrl, { params: params });
+
+ // Mapping Result
+ const mappedProducts = productMappingSolr(
+ result.data.response.docs,
+ false
+ );
- const parameter = [
- 'q=*:*',
- `rows=${limit}`,
- `start=${offset}`,
- 'fl=product_id_i,name_s,default_code_s,image_s,category_name',
- 'wt=json',
- 'omitHeader=true',
- ];
+ const finalResponse = {
+ ...result.data,
+ response: {
+ ...result.data.response,
+ products: mappedProducts
+ }
+ };
- // const parameter = [
- // 'q=*:*',
- // `rows=${limit}`,
- // `start=${offset}`,
+ delete finalResponse.response.docs;
+ const camelCasedData = camelcaseObjectDeep(finalResponse);
- // // ❌ EXCLUDE PROMOTION
- // 'fq=-(name_s:*promotion* OR display_name_s:*promotion* OR variants_name_t:*promotion*)',
+ return res.status(200).json(camelCasedData);
- // // ❌ EXCLUDE DUMMY PRODUCT
- // 'fq=-(name_s:*dummy* OR display_name_s:*dummy* OR variants_name_t:*dummy* OR default_code_s:A.*)',
+ } catch (e) {
+ console.error('[COMPARE SEARCH ERROR]', e.message);
+ if (e.response && e.response.data) {
+ // Log detail error dari Solr
+ console.error('[SOLR DETAILS]:', JSON.stringify(e.response.data, null, 2));
+ }
+ return res.status(200).json({ response: { products: [], numFound: 0 } });
+ }
+ }
- // 'fl=product_id_i,name_s,default_code_s,image_s,category_name',
- // 'wt=json',
- // 'omitHeader=true',
- // ];
+ // ============================================================
+ // LOGIC KHUSUS UPSELL (KODE LAMA ANDA)
+ // ============================================================
+ if (source === 'upsell') {
+ try {
+ // Ambil fq dari query (format: product_id_i:(...))
+ // Pastikan fq adalah string tunggal
+ let fqUpsell = Array.isArray(fq) ? fq.join(' OR ') : fq;
+ fqUpsell = decodeURIComponent(fqUpsell);
- const solrUrl =
- process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&');
+ const parameter = [
+ 'q=*:*',
+ `rows=${limit}`,
+ 'wt=json',
+ 'indent=true',
+ 'defType=edismax',
+ // Filter Query khusus Upsell
+ `fq=${encodeURIComponent(fqUpsell)}`,
+ // Tetap filter yang publish & ada harga agar produk valid
+ `fq=${encodeURIComponent('-publish_b:false')}`,
+ `fq=${encodeURIComponent('price_tier1_v2_f:[1 TO *]')}`
+ ];
- // console.log('[SITEMAP SOLR QUERY]', solrUrl);
+ // PENTING: SEARCH DI CORE 'VARIANTS'
+ const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&');
- const result = await axios(solrUrl, { timeout: 25000 });
+ const result = await axios(solrUrl);
- // mapping seperti biasa
- result.data.response.products = productMappingSolr(
+ // 1. Mapping dasar
+ const mappedProducts = productMappingSolr(
result.data.response.docs,
false
);
- delete result.data.response.docs;
+ // 2. FIX URL LINK: Override ID Varian dengan Template ID
+ const rawDocs = result.data.response.docs;
+
+ const fixedProducts = mappedProducts.map((p, index) => {
+ const raw = rawDocs[index];
+ if (raw && raw.template_id_i) {
+ return {
+ ...p,
+ id: raw.template_id_i, // Ganti ID Varian jadi ID Template agar link valid
+ variantId: raw.product_id_i
+ };
+ }
+ return p;
+ });
+
+ const finalResponse = {
+ ...result.data,
+ response: {
+ ...result.data.response,
+ products: fixedProducts
+ }
+ };
+
+ delete finalResponse.response.docs;
+ const camelCasedData = camelcaseObjectDeep(finalResponse);
+
+ return res.status(200).json(camelCasedData);
- result.data = camelcaseObjectDeep(result.data);
+ } catch (e) {
+ console.error('[UPSELL ERROR]', e.response?.data || e.message);
+ return res.status(200).json({ response: { products: [], numFound: 0 } });
+ }
+ }
+ // ============================================================
+ // SITEMAP (KODE LAMA ANDA)
+ // ============================================================
+ if (source === 'sitemap') {
+ try {
+ const offset = (page - 1) * limit;
+ const parameter = [
+ 'q=*:*',
+ `rows=${limit}`,
+ `start=${offset}`,
+ 'fl=product_id_i,name_s,default_code_s,image_s,category_name',
+ 'wt=json',
+ 'omitHeader=true',
+ ];
+ const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&');
+ const result = await axios(solrUrl, { timeout: 25000 });
+ result.data.response.products = productMappingSolr(result.data.response.docs, false);
+ delete result.data.response.docs;
+ result.data = camelcaseObjectDeep(result.data);
return res.status(200).json(result.data);
} catch (e) {
- console.error('[SITEMAP ERROR]', e);
return res.status(500).json({ error: 'Sitemap query failed' });
}
}
// ============================================================
- // SEARCH NORMAL
+ // SEARCH NORMAL (KODE LAMA ANDA)
// ============================================================
let paramOrderBy = '';
switch (orderBy) {
- case 'flashsale-discount-desc':
- paramOrderBy += 'flashsale_discount_f DESC';
- break;
- case 'price-asc':
- paramOrderBy += 'price_tier1_v2_f ASC';
- break;
- case 'price-desc':
- paramOrderBy += 'price_tier1_v2_f DESC';
- break;
- case 'popular':
- paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,';
- break;
- case 'popular-weekly':
- paramOrderBy += 'search_rank_weekly_i DESC';
- break;
- case 'stock':
- paramOrderBy += 'product_rating_f DESC, stock_total_f DESC';
- break;
- case 'flashsale-price-asc':
- paramOrderBy += 'flashsale_price_f ASC';
- break;
- default:
- paramOrderBy += '';
- break;
+ case 'flashsale-discount-desc': paramOrderBy += 'flashsale_discount_f DESC'; break;
+ case 'price-asc': paramOrderBy += 'price_tier1_v2_f ASC'; break;
+ case 'price-desc': paramOrderBy += 'price_tier1_v2_f DESC'; break;
+ case 'popular': paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; break;
+ case 'popular-weekly': paramOrderBy += 'search_rank_weekly_i DESC'; break;
+ case 'stock': paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; break;
+ case 'flashsale-price-asc': paramOrderBy += 'flashsale_price_f ASC'; break;
+ default: paramOrderBy += ''; break;
}
let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/);
let newQ = escapeSolrQuery(q);
- const formattedQuery = `(${newQ
- .split(' ')
- .map((term) => (term.length < 2 ? term : `${term}*`))
- .join(' ')})`;
-
- const mm =
- checkQ.length > 2
- ? checkQ.length > 5
- ? '55%'
- : '85%'
- : `${checkQ.length}`;
+ const formattedQuery = `(${newQ.split(' ').map((term) => (term.length < 2 ? term : `${term}*`)).join(' ')})`;
+ const mm = checkQ.length > 2 ? (checkQ.length > 5 ? '55%' : '85%') : `${checkQ.length}`;
const filterQueries = [
'-publish_b:false',
@@ -128,23 +251,19 @@ export default async function handler(req, res) {
'price_tier1_v2_f:[1 TO *]',
];
- if (orderBy === 'stock') {
- filterQueries.push('stock_total_f:[1 TO *]');
- }
+ if (orderBy === 'stock') filterQueries.push('stock_total_f:[1 TO *]');
- if (fq && source != 'similar' && typeof fq != 'string') {
- fq.push(...filterQueries);
+ // Handle 'fq' parameter from request
+ let finalFq = [...filterQueries];
+ if (fq) {
+ if (Array.isArray(fq)) finalFq.push(...fq);
+ else finalFq.push(fq);
}
- const fq_ = filterQueries.join(' AND ');
-
let keywords = newQ;
if (source === 'similar' || checkQ.length < 3) {
- if (checkQ.length < 2 || checkQ[1].length < 2) {
- keywords = newQ;
- } else {
- keywords = newQ + '*';
- }
+ if (checkQ.length < 2 || checkQ[1].length < 2) keywords = newQ;
+ else keywords = newQ + '*';
} else {
keywords = formattedQuery;
}
@@ -164,16 +283,16 @@ export default async function handler(req, res) {
`start=${parseInt(offset)}`,
`rows=${limit}`,
`sort=${paramOrderBy}`,
- `fq=${encodeURIComponent(fq_)}`,
`mm=${encodeURIComponent(mm)}`,
];
+ // Masukkan semua Filter Query (fq)
+ finalFq.forEach(f => {
+ parameter.push(`fq=${encodeURIComponent(f)}`);
+ });
+
if (priceFrom > 0 || priceTo > 0) {
- parameter.push(
- `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${
- priceTo == '' ? '*' : priceTo
- }]`
- );
+ parameter.push(`fq=price_tier1_v2_f:[${priceFrom || '*'} TO ${priceTo || '*'}]`);
}
let { auth } = req.cookies;
@@ -183,70 +302,32 @@ export default async function handler(req, res) {
}
if (brand) {
- const brandExpr = brand
- .split(',')
- .map(
- (manufacturer) =>
- `manufacture_name:"${encodeURIComponent(manufacturer)}"`
- )
- .join(' OR ');
+ const brandExpr = brand.split(',').map(m => `manufacture_name:"${encodeURIComponent(m)}"`).join(' OR ');
parameter.push(`fq={!tag=brand}(${brandExpr})`);
}
if (category) {
- const catExpr = category
- .split(',')
- .map((cat) => `category_name:"${encodeURIComponent(cat)}"`)
- .join(' OR ');
+ const catExpr = category.split(',').map(c => `category_name:"${encodeURIComponent(c)}"`).join(' OR ');
parameter.push(`fq={!tag=cat}(${catExpr})`);
}
if (stock) parameter.push(`fq=stock_total_f:(1 TO *)`);
- if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`);
-
- if (Array.isArray(fq))
- parameter = parameter.concat(
- fq.map((val) => `fq=${encodeURIComponent(val)}`)
- );
-
- const solrUrl =
- process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&');
-
- const result = await axios(solrUrl);
+ // SEARCH NORMAL: DEFAULT KE CORE 'PRODUCT'
+ const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&');
try {
+ const result = await axios(solrUrl);
result.data.response.products = productMappingSolr(
result.data.response.docs,
auth?.pricelist || false
);
- result.data.responseHeader.params.start = parseInt(
- result.data.responseHeader.params.start
- );
- result.data.responseHeader.params.rows = parseInt(
- result.data.responseHeader.params.rows
- );
+ result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start);
+ result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows);
delete result.data.response.docs;
result.data = camelcaseObjectDeep(result.data);
res.status(200).json(result.data);
} catch (error) {
res.status(400).json({ error: error.message });
}
-}
-
-const escapeSolrQuery = (query) => {
- if (query == '*') return query;
-
- query = query.replace(/-/g, ' ');
-
- const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
- const words = query.split(/\s+/);
- const escapedWords = words.map((word) => {
- if (specialChars.test(word)) {
- return word.replace(specialChars, '\\$1');
- }
- return word;
- });
-
- return escapedWords.join(' ');
-};
+} \ No newline at end of file