Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ apps/api/test-baselinker-categories.js
apps/api/check-count.js
apps/api/sync-images.js

# Promo codes
apps/api/generate-promo-codes.js
apps/api/promo-codes-*

# ============================================
# 🔒 SECURITY - Sensitive files
# ============================================
Expand Down
112 changes: 112 additions & 0 deletions apps/api/check-bl-orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Sprawdź status synchronizacji zamówień do Baselinkera
*/

import { prisma } from './src/db';

async function main() {
console.log('📊 Status zamówień i synchronizacji do Baselinkera\n');

// 1. Podsumowanie po statusach
const summary = await prisma.order.groupBy({
by: ['paymentStatus', 'status'],
_count: true,
});

console.log('📈 Statystyki zamówień:');
console.table(summary.map(s => ({
'Payment Status': s.paymentStatus,
'Order Status': s.status,
'Count': s._count,
})));

// 2. Opłacone zamówienia bez sync do Baselinkera
const unsyncedPaid = await prisma.order.findMany({
where: {
paymentStatus: 'PAID',
baselinkerOrderId: null,
},
select: {
id: true,
orderNumber: true,
status: true,
total: true,
createdAt: true,
items: {
select: {
productName: true,
quantity: true,
variant: {
select: {
product: {
select: {
baselinkerProductId: true,
}
}
}
}
}
}
},
orderBy: { createdAt: 'desc' },
});

console.log(`\n❌ Opłacone zamówienia BEZ sync do Baselinkera (${unsyncedPaid.length}):`);
for (const order of unsyncedPaid) {
console.log(`\n 📦 ${order.orderNumber} | ${order.status} | ${order.total} PLN`);
console.log(` ID: ${order.id}`);
console.log(` Created: ${order.createdAt.toISOString()}`);
console.log(` Products:`);
for (const item of order.items) {
const blId = item.variant?.product?.baselinkerProductId || 'BRAK';
console.log(` - ${item.productName} x${item.quantity} (BL: ${blId})`);
}
}

// 3. Ostatnie zsynchronizowane zamówienia
const syncedOrders = await prisma.order.findMany({
where: {
baselinkerOrderId: { not: null },
},
select: {
orderNumber: true,
status: true,
paymentStatus: true,
baselinkerOrderId: true,
baselinkerSyncedAt: true,
},
orderBy: { baselinkerSyncedAt: 'desc' },
take: 10,
});

console.log(`\n✅ Ostatnie zsynchronizowane zamówienia (${syncedOrders.length}):`);
for (const order of syncedOrders) {
console.log(` ${order.orderNumber} → BL#${order.baselinkerOrderId} | ${order.baselinkerSyncedAt?.toISOString()}`);
}

// 4. Sprawdź konfigurację Baselinkera
const config = await prisma.baselinkerConfig.findFirst({
select: {
syncEnabled: true,
inventoryId: true,
createdAt: true,
updatedAt: true,
},
});

console.log('\n⚙️ Konfiguracja Baselinkera:');
if (config) {
console.log(` Sync enabled: ${config.syncEnabled}`);
console.log(` Inventory ID: ${config.inventoryId}`);
console.log(` Updated: ${config.updatedAt?.toISOString()}`);
} else {
console.log(' ❌ BRAK KONFIGURACJI BASELINKERA!');
}

await prisma.$disconnect();
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,11 @@ export interface IBaselinkerProvider {
inventoryId: string,
products: Record<string, Record<string, number>>
): Promise<void>;

/**
* Set order status in Baselinker
* @param orderId - Baselinker order ID
* @param statusId - New status ID
*/
setOrderStatus(orderId: string | number, statusId: number): Promise<void>;
}
16 changes: 16 additions & 0 deletions apps/api/src/providers/baselinker/baselinker.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,22 @@ export class BaselinkerProvider implements IBaselinkerProvider {
console.log('[Baselinker] Stock updated successfully');
}

/**
* Set order status in Baselinker
* @param orderId - Baselinker order ID
* @param statusId - New status ID
*/
async setOrderStatus(orderId: string | number, statusId: number): Promise<void> {
console.log(`[Baselinker] Setting order ${orderId} status to ${statusId}`);

await this.request('setOrderStatus', {
order_id: typeof orderId === 'string' ? parseInt(orderId, 10) : orderId,
status_id: statusId,
});

console.log(`[Baselinker] Order ${orderId} status updated to ${statusId}`);
}

/**
* Split array into chunks
*/
Expand Down
101 changes: 87 additions & 14 deletions apps/api/src/services/baselinker-orders.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,18 @@ const WAREHOUSE_MAPPING: Record<string, string> = {
};

/**
* Default order status ID for new orders in Baselinker
* 65342 = "Nowe zamówienia"
* Baselinker order status IDs
*/
const DEFAULT_ORDER_STATUS_ID = 65342;
const BL_STATUS = {
UNPAID: 65823, // "Nieopłacone" - czerwony
NEW_ORDER: 65342, // "Nowe zamówienia" - niebieski (po opłaceniu)
CANCELLED: 65816, // "Zwroty/Anulowa" - czerwony
};

/**
* Default order status ID for new (unpaid) orders in Baselinker
*/
const DEFAULT_ORDER_STATUS_ID = BL_STATUS.UNPAID;

/**
* Detect warehouse inventory ID from product data
Expand Down Expand Up @@ -76,10 +84,12 @@ export interface OrderSyncResult {
}

export interface SyncOrderToBaselinkerOptions {
/** Baselinker order status ID (default: 65342 - Nowe zamówienia) */
/** Baselinker order status ID (default: Nieopłacone) */
orderStatusId?: number;
/** Force sync even if already synced */
force?: boolean;
/** Skip payment check - for initial order creation */
skipPaymentCheck?: boolean;
}

// ============================================
Expand All @@ -89,7 +99,8 @@ export interface SyncOrderToBaselinkerOptions {
export class BaselinkerOrdersService {
/**
* Sync a single order to Baselinker
* This should be called ONLY after payment is confirmed
* Orders are synced immediately after creation with status "Nieopłacone"
* After payment, the status is updated to "Nowe zamówienia"
*
* @param orderId - Local order ID
* @param options - Sync options
Expand All @@ -98,7 +109,7 @@ export class BaselinkerOrdersService {
orderId: string,
options: SyncOrderToBaselinkerOptions = {}
): Promise<OrderSyncResult> {
const { orderStatusId = DEFAULT_ORDER_STATUS_ID, force = false } = options;
const { orderStatusId = DEFAULT_ORDER_STATUS_ID, force = false, skipPaymentCheck = false } = options;

try {
// 1. Get order with all related data
Expand Down Expand Up @@ -140,14 +151,11 @@ export class BaselinkerOrdersService {
};
}

// 3. Verify payment is confirmed - CRITICAL CHECK
if (order.paymentStatus !== 'PAID') {
console.warn(`[BaselinkerOrders] Order ${orderId} has paymentStatus=${order.paymentStatus}, skipping sync`);
return {
success: false,
orderId,
error: `Order payment status is ${order.paymentStatus}, expected PAID. Sync only after payment confirmation.`,
};
// 3. Skip payment check for new orders - they go to Baselinker as "Nieopłacone"
// Payment check is only enforced when we need to update status to "paid"
if (!skipPaymentCheck && order.paymentStatus !== 'PAID' && !force) {
// If order is not paid and we're not skipping check, just sync with unpaid status
console.log(`[BaselinkerOrders] Order ${orderId} is unpaid, syncing with Nieopłacone status`);
}

// 4. Get Baselinker configuration
Expand Down Expand Up @@ -409,7 +417,72 @@ export class BaselinkerOrdersService {

return mappings[method] || method;
}

/**
* Update order status in Baselinker after payment
* Changes status from "Nieopłacone" to "Nowe zamówienia"
*/
async markOrderAsPaid(orderId: string): Promise<OrderSyncResult> {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
select: {
id: true,
orderNumber: true,
baselinkerOrderId: true,
paymentStatus: true
},
});

if (!order) {
return { success: false, orderId, error: 'Order not found' };
}

if (!order.baselinkerOrderId) {
console.warn(`[BaselinkerOrders] Order ${orderId} has no Baselinker ID, cannot update status`);
return { success: false, orderId, error: 'Order not synced to Baselinker yet' };
}

// Get Baselinker configuration
const config = await prisma.baselinkerConfig.findFirst({
where: { syncEnabled: true },
});

if (!config) {
return { success: false, orderId, error: 'Baselinker not configured' };
}

const apiToken = decryptToken(
config.apiTokenEncrypted,
config.encryptionIv,
config.authTag
);

const provider = createBaselinkerProvider({
apiToken,
inventoryId: config.inventoryId,
});

// Update status to "Nowe zamówienia" (paid)
await provider.setOrderStatus(order.baselinkerOrderId, BL_STATUS.NEW_ORDER);

console.log(`[BaselinkerOrders] Order ${order.orderNumber} marked as paid in Baselinker (status: ${BL_STATUS.NEW_ORDER})`);

return {
success: true,
orderId,
baselinkerOrderId: order.baselinkerOrderId,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('[BaselinkerOrders] Failed to mark order as paid:', orderId, error);
return { success: false, orderId, error: errorMessage };
}
}
}

// Export singleton instance
export const baselinkerOrdersService = new BaselinkerOrdersService();

// Export status constants for use in other services
export { BL_STATUS };
35 changes: 31 additions & 4 deletions apps/api/src/services/orders.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,22 @@ export class OrdersService {
});
}

// Sync order to Baselinker immediately with "Nieopłacone" status
// This is done outside transaction to not block order creation
setTimeout(() => {
baselinkerOrdersService.syncOrderToBaselinker(order.id, { skipPaymentCheck: true })
.then((syncResult) => {
if (syncResult.success) {
console.log(`[OrdersService] Order ${order.orderNumber} synced to Baselinker as unpaid (BL ID: ${syncResult.baselinkerOrderId})`);
} else {
console.error(`[OrdersService] Failed to sync order ${order.orderNumber} to Baselinker:`, syncResult.error);
}
})
.catch((err) => {
console.error(`[OrdersService] Baselinker sync error for order ${order.orderNumber}:`, err);
});
}, 100);

return order;
});
}
Expand Down Expand Up @@ -527,7 +543,7 @@ export class OrdersService {

// After successful payment simulation, sync to Baselinker
if (updatedOrder) {
console.log(`[DEV] Triggering Baselinker sync for simulated payment, order ${id}`);
console.log(`[DEV] Triggering Baselinker status update for simulated payment, order ${id}`);

// Update product sales count for popularity tracking
for (const item of updatedOrder.items) {
Expand All @@ -541,12 +557,23 @@ export class OrdersService {
}
}

baselinkerOrdersService.syncOrderToBaselinker(id)
// Update Baselinker order status from "Nieopłacone" to "Nowe zamówienia"
baselinkerOrdersService.markOrderAsPaid(id)
.then((syncResult) => {
if (syncResult.success) {
console.log(`[DEV] Order ${id} synced to Baselinker (BL ID: ${syncResult.baselinkerOrderId})`);
console.log(`[DEV] Order ${id} marked as paid in Baselinker`);
} else {
console.error(`[DEV] Failed to sync order ${id} to Baselinker:`, syncResult.error);
// If order wasn't synced yet, sync it now with paid status
console.warn(`[DEV] Could not update status, trying full sync: ${syncResult.error}`);
return baselinkerOrdersService.syncOrderToBaselinker(id, {
orderStatusId: 65342, // Nowe zamówienia (paid)
skipPaymentCheck: true
});
}
})
.then((syncResult) => {
if (syncResult && syncResult.success) {
console.log(`[DEV] Order ${id} synced to Baselinker (BL ID: ${syncResult.baselinkerOrderId})`);
}
})
.catch((err) => {
Expand Down
Loading