();
+
+ for (const line of items) {
+ const key = line.productVariantId || `${line.sku}|${line.productName}`;
+ const existing = groups.get(key);
+ if (existing) {
+ existing.lines.push(line);
+ existing.totalQuantity += line.quantity;
+ if (line.isCollected) {
+ existing.collectedQuantity += line.quantity;
+ } else {
+ existing.pendingQuantity += line.quantity;
+ }
+ mergeWarehouseStock(existing.warehouseStock, line.warehouseStock);
+ continue;
+ }
+
+ groups.set(key, {
+ productVariantId: line.productVariantId,
+ productName: line.productName,
+ variantColor: line.variantColor,
+ sku: line.sku,
+ totalQuantity: line.quantity,
+ pendingQuantity: line.isCollected ? 0 : line.quantity,
+ collectedQuantity: line.isCollected ? line.quantity : 0,
+ lines: [line],
+ warehouseStock: line.warehouseStock.map((ws) => ({ ...ws })),
+ });
+ }
+
+ return [...groups.values()].sort((a, b) => {
+ const byName = a.productName.localeCompare(b.productName);
+ if (byName !== 0) {
+ return byName;
+ }
+ return a.sku.localeCompare(b.sku);
+ });
+}
+
+function mergeWarehouseStock(
+ target: PickListWarehouseQty[],
+ incoming: PickListWarehouseQty[],
+): void {
+ const map = new Map(target.map((ws) => [ws.warehouseId, ws]));
+ for (const ws of incoming) {
+ if (!map.has(ws.warehouseId)) {
+ const copy = { ...ws };
+ map.set(ws.warehouseId, copy);
+ target.push(copy);
+ }
+ }
+}
+
export function canCollectPickLine(
line: PickListLine,
warehouseId: string,
diff --git a/src/admin/src/app/features/orders/order.models.ts b/src/admin/src/app/features/orders/order.models.ts
index 307b672..50a89e1 100644
--- a/src/admin/src/app/features/orders/order.models.ts
+++ b/src/admin/src/app/features/orders/order.models.ts
@@ -159,6 +159,18 @@ export interface PickListOrderGroup {
collectedCount: number;
}
+export interface PickListProductGroup {
+ productVariantId: string;
+ productName: string;
+ variantColor: string;
+ sku: string;
+ totalQuantity: number;
+ pendingQuantity: number;
+ collectedQuantity: number;
+ lines: PickListLine[];
+ warehouseStock: PickListWarehouseQty[];
+}
+
export interface OrderInvoice {
invoiceNumber: string;
issueDate: string;
diff --git a/src/admin/src/app/features/orders/orders-admin.component.html b/src/admin/src/app/features/orders/orders-admin.component.html
index ef0fd43..d3bfd56 100644
--- a/src/admin/src/app/features/orders/orders-admin.component.html
+++ b/src/admin/src/app/features/orders/orders-admin.component.html
@@ -175,33 +175,11 @@
| {{ o.orderNumber }} |
-
-
- {{ 'orders.sourceShort.' + (o.source || 'shop') | translate }}
-
-
- {{ 'orders.carrierShort.' + (o.shippingCarrier || 'inPost') | translate }}
- ·
- {{
- o.shippingServiceType === 'courier'
- ? ('orders.serviceShort.courier' | translate)
- : ('orders.serviceShort.parcelLocker' | translate)
- }}
-
-
+
|
{{ o.createdAt | date: 'yyyy-MM-dd HH:mm' }}
@@ -268,7 +246,25 @@
· {{ 'orders.pendingItems' | translate }}: {{ pl.pendingCount }}
· {{ 'orders.collectedItems' | translate }}: {{ pl.collectedCount }}
-
+
+ @if (pickListMode() === 'order') {
@for (group of pickListGroups(pl.items); track group.orderId) {
}
+ } @else {
+ @for (
+ group of pickListProductGroups(pl.items);
+ track group.productVariantId || group.sku
+ ) {
+
+
+
+
+ {{ group.productName }}
+ @if (group.variantColor) {
+
+ ({{ group.variantColor }})
+
+ }
+
+ {{ group.sku }}
+
+ {{
+ 'orders.pickTotalQty'
+ | translate: { count: group.totalQuantity }
+ }}
+ · {{ 'orders.pendingItems' | translate }}:
+ {{ group.pendingQuantity }}
+ · {{ 'orders.collectedItems' | translate }}:
+ {{ group.collectedQuantity }}
+
+
+ @if (group.pendingQuantity > 0) {
+
+
+
+
+
+
+
+ } @else {
+
+ {{ 'orders.productCollected' | translate }}
+
+ }
+
+
+
+
+
+ | {{ 'orders.order' | translate }} |
+ {{ 'orders.qty' | translate }} |
+ {{ 'orders.warehouseStock' | translate }} |
+ {{ 'orders.pickFrom' | translate }} |
+ {{ 'orders.status' | translate }} |
+
+
+
+ @for (line of group.lines; track line.orderLineId) {
+
+ |
+ #{{ line.orderNumber }}
+ |
+ {{ line.quantity }} |
+
+ @for (ws of line.warehouseStock; track ws.warehouseId) {
+
+ {{ ws.warehouseName }} ({{ ws.warehouseCode }}):
+ {{ ws.quantity }}
+
+ } @empty {
+ —
+ }
+ |
+
+ @if (!line.isCollected) {
+
+ } @else if (line.pickedFromWarehouseName) {
+
+ {{ line.pickedFromWarehouseName }}
+ ({{ line.pickedFromWarehouseCode }})
+
+ }
+ |
+
+ @if (line.isCollected) {
+
+ {{ 'orders.collected' | translate }}
+
+ } @else {
+
+ }
+ |
+
+ }
+
+
+
+ } @empty {
+
+ {{ 'orders.pickListEmpty' | translate }}
+
+ }
+ }
}
}
diff --git a/src/admin/src/app/features/orders/orders-admin.component.ts b/src/admin/src/app/features/orders/orders-admin.component.ts
index cde3d19..5e5b5b5 100644
--- a/src/admin/src/app/features/orders/orders-admin.component.ts
+++ b/src/admin/src/app/features/orders/orders-admin.component.ts
@@ -12,6 +12,7 @@ import { ActivatedRoute } from '@angular/router';
import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe';
import { TranslatePipe } from '../../core/i18n/translate.pipe';
import { ApiService } from '../../core/api.service';
+import { OrderChannelIconsComponent } from './order-channel-icons.component';
import { OrderDetailModalComponent } from './order-detail-modal.component';
import { OrderInvoiceModalComponent } from './order-invoice-modal.component';
import {
@@ -26,6 +27,7 @@ import {
PickListResult,
PickListLine,
PickListOrderGroup,
+ PickListProductGroup,
} from './order.models';
import {
applyOrderWarehouse,
@@ -35,6 +37,7 @@ import {
canUseWarehouseForOrder,
defaultWarehouseForLine,
groupPickListByOrder,
+ groupPickListByProduct,
orderWarehouseOptions,
} from './order-pick.utils';
import { AdminListToolbarComponent } from '../../shared/admin-list-toolbar.component';
@@ -43,6 +46,7 @@ import { AdminSkeletonComponent } from '../../shared/admin-skeleton.component';
import { ToastService } from '../../shared/toast.service';
type OrdersTab = 'list' | 'pickList';
+type PickListMode = 'order' | 'product';
type SortBy = 'date' | 'total' | 'number';
type SortDir = 'asc' | 'desc';
@@ -58,6 +62,7 @@ type SortDir = 'asc' | 'desc';
AdminListToolbarComponent,
AdminPaginationComponent,
AdminSkeletonComponent,
+ OrderChannelIconsComponent,
OrderDetailModalComponent,
OrderInvoiceModalComponent,
],
@@ -78,6 +83,7 @@ export class OrdersAdminComponent implements OnInit {
readonly pickList = signal(null);
readonly pickListLoading = signal(false);
+ readonly pickListMode = signal('order');
readonly showCollected = signal(false);
readonly collectingLineId = signal(null);
readonly collectingOrderId = signal(null);
@@ -200,6 +206,10 @@ export class OrdersAdminComponent implements OnInit {
this.loadPickList();
}
+ setPickListMode(mode: PickListMode): void {
+ this.pickListMode.set(mode);
+ }
+
setWarehouse(lineId: string, warehouseId: string): void {
this.selectedWarehouse.set(lineId, warehouseId);
this.#cdr.markForCheck();
@@ -217,6 +227,99 @@ export class OrdersAdminComponent implements OnInit {
return groupPickListByOrder(items);
}
+ pickListProductGroups(items: PickListLine[]): PickListProductGroup[] {
+ return groupPickListByProduct(items);
+ }
+
+ setProductWarehouse(
+ group: PickListProductGroup,
+ warehouseId: string,
+ ): void {
+ for (const line of group.lines) {
+ if (!line.isCollected) {
+ this.selectedWarehouse.set(line.orderLineId, warehouseId);
+ }
+ }
+ this.#cdr.markForCheck();
+ }
+
+ getProductWarehouse(group: PickListProductGroup): string {
+ const pending = group.lines.find((line) => !line.isCollected);
+ if (!pending) {
+ return '';
+ }
+ return this.getWarehouse(pending.orderLineId);
+ }
+
+ canCollectProductGroup(group: PickListProductGroup): boolean {
+ const pending = group.lines.filter((line) => !line.isCollected);
+ if (pending.length === 0) {
+ return false;
+ }
+
+ const warehouseId = this.getWarehouse(pending[0].orderLineId);
+ if (!warehouseId) {
+ return false;
+ }
+
+ if (
+ !pending.every(
+ (line) => this.getWarehouse(line.orderLineId) === warehouseId,
+ )
+ ) {
+ return false;
+ }
+
+ const needed = pending.reduce((sum, line) => sum + line.quantity, 0);
+ const stock = group.warehouseStock.find(
+ (ws) => ws.warehouseId === warehouseId,
+ );
+ return !!stock && stock.quantity >= needed;
+ }
+
+ handleCollectProduct(group: PickListProductGroup): void {
+ if (!this.canCollectProductGroup(group)) {
+ return;
+ }
+
+ const pending = group.lines.filter((line) => !line.isCollected);
+ if (pending.length === 0) {
+ return;
+ }
+
+ this.collectingOrderId.set(group.productVariantId || group.sku);
+ let remaining = pending.length;
+ let failed = false;
+
+ for (const line of pending) {
+ const warehouseId = this.getWarehouse(line.orderLineId);
+ this.#api
+ .post(`/orders/admin/lines/${line.orderLineId}/collect`, {
+ warehouseId,
+ })
+ .subscribe({
+ next: () => {
+ remaining -= 1;
+ if (remaining === 0) {
+ this.collectingOrderId.set(null);
+ if (!failed) {
+ this.loadPickList();
+ this.loadOrders();
+ }
+ }
+ },
+ error: () => {
+ failed = true;
+ remaining -= 1;
+ if (remaining === 0) {
+ this.collectingOrderId.set(null);
+ this.loadPickList();
+ }
+ },
+ });
+ }
+ }
+
getOrderWarehouse(orderId: string): string {
return this.selectedOrderWarehouse.get(orderId) ?? '';
}
diff --git a/src/admin/src/app/features/products-admin.component.ts b/src/admin/src/app/features/products-admin.component.ts
index b18de40..8a6c195 100644
--- a/src/admin/src/app/features/products-admin.component.ts
+++ b/src/admin/src/app/features/products-admin.component.ts
@@ -89,6 +89,8 @@ export class ProductsAdminComponent implements OnInit {
this.filterLowStock =
this.#route.snapshot.queryParamMap.get('lowStock') === '1';
+ this.filterCategoryId =
+ this.#route.snapshot.queryParamMap.get('categoryId') ?? '';
this.loadProducts();
}
@@ -309,7 +311,11 @@ export class ProductsAdminComponent implements OnInit {
#syncLowStockQuery(): void {
void this.#router.navigate([], {
relativeTo: this.#route,
- queryParams: this.filterLowStock ? { lowStock: '1' } : {},
+ queryParams: {
+ lowStock: this.filterLowStock ? '1' : null,
+ categoryId: this.filterCategoryId || null,
+ },
+ queryParamsHandling: 'merge',
replaceUrl: true,
});
}
diff --git a/src/admin/src/app/features/settings/settings-admin.component.html b/src/admin/src/app/features/settings/settings-admin.component.html
index e3395bf..c80ea81 100644
--- a/src/admin/src/app/features/settings/settings-admin.component.html
+++ b/src/admin/src/app/features/settings/settings-admin.component.html
@@ -234,7 +234,16 @@
{{ 'settings.shippingTitle' | translate }}
-
+
+
@@ -256,8 +265,17 @@
/>
|
-
- {{ 'settings.sandbox' | translate }}
+
+ {{
+ (form.shippingUseSandbox
+ ? 'settings.sandbox'
+ : 'settings.live'
+ ) | translate
+ }}
|
@@ -272,8 +290,17 @@
/>
-
- {{ 'settings.sandbox' | translate }}
+
+ {{
+ (form.shippingUseSandbox
+ ? 'settings.sandbox'
+ : 'settings.live'
+ ) | translate
+ }}
|
@@ -288,15 +315,24 @@
/>
-
- {{ 'settings.sandbox' | translate }}
+
+ {{
+ (form.shippingUseSandbox
+ ? 'settings.sandbox'
+ : 'settings.live'
+ ) | translate
+ }}
|
-
+
{{ 'settings.shippingHint' | translate }}
@@ -313,6 +349,335 @@
{{ 'settings.integrationsHint' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Allegro
diff --git a/src/admin/src/app/features/settings/settings-admin.component.ts b/src/admin/src/app/features/settings/settings-admin.component.ts
index 0b51696..3224995 100644
--- a/src/admin/src/app/features/settings/settings-admin.component.ts
+++ b/src/admin/src/app/features/settings/settings-admin.component.ts
@@ -36,6 +36,7 @@ interface StoreSettingsApi {
locales: { pl: boolean; en: boolean; de: boolean; cs: boolean };
lowStockThreshold: number;
carriers: { inPost: boolean; dhl: boolean; dpd: boolean };
+ shippingUseSandbox: boolean;
emailFromName: string;
emailFromAddress: string;
emailReplyTo: string;
@@ -61,6 +62,39 @@ interface StoreSettingsApi {
invoiceSeries?: string | null;
autoInvoice: boolean;
};
+ inPost: {
+ hasShipXToken: boolean;
+ organizationId?: string | null;
+ shipXBaseUrl?: string | null;
+ hasGeowidgetPublicToken: boolean;
+ hasGeowidgetSandboxToken: boolean;
+ geowidgetConfig?: string | null;
+ geowidgetSdkBaseUrl?: string | null;
+ };
+ dhl: {
+ hasApiKey: boolean;
+ hasApiSecret: boolean;
+ apiBaseUrl?: string | null;
+ widgetUrl?: string | null;
+ };
+ dpd: {
+ login?: string | null;
+ hasPassword: boolean;
+ fid?: string | null;
+ apiBaseUrl?: string | null;
+ widgetUrl?: string | null;
+ };
+ payU: {
+ useSandbox: boolean;
+ posId?: string | null;
+ clientId?: string | null;
+ hasClientSecret: boolean;
+ hasSecondKey: boolean;
+ oauthUrl?: string | null;
+ ordersUrl?: string | null;
+ notifyUrl?: string | null;
+ continueUrl?: string | null;
+ };
}
@Component({
@@ -77,12 +111,7 @@ export class SettingsAdminComponent implements OnInit {
readonly section = signal ('store');
readonly loading = signal(false);
readonly saving = signal(false);
- form: AdminSettings = {
- ...DEFAULT_ADMIN_SETTINGS,
- allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
- amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
- ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
- };
+ form: AdminSettings = this.#cloneDefaults();
ngOnInit(): void {
this.load();
@@ -133,6 +162,19 @@ export class SettingsAdminComponent implements OnInit {
});
}
+ #cloneDefaults(): AdminSettings {
+ return {
+ ...DEFAULT_ADMIN_SETTINGS,
+ allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
+ amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
+ ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
+ inPost: { ...DEFAULT_ADMIN_SETTINGS.inPost },
+ dhl: { ...DEFAULT_ADMIN_SETTINGS.dhl },
+ dpd: { ...DEFAULT_ADMIN_SETTINGS.dpd },
+ payU: { ...DEFAULT_ADMIN_SETTINGS.payU },
+ };
+ }
+
#fromApi(res: StoreSettingsApi): AdminSettings {
return {
storeName: res.storeName,
@@ -153,6 +195,7 @@ export class SettingsAdminComponent implements OnInit {
dhl: res.carriers.dhl,
dpd: res.carriers.dpd,
},
+ shippingUseSandbox: res.shippingUseSandbox ?? true,
emailFromName: res.emailFromName,
emailFromAddress: res.emailFromAddress,
emailReplyTo: res.emailReplyTo,
@@ -182,6 +225,48 @@ export class SettingsAdminComponent implements OnInit {
autoInvoice: res.ifirma?.autoInvoice ?? false,
hasApiKey: res.ifirma?.hasApiKey ?? false,
},
+ inPost: {
+ shipXToken: '',
+ organizationId: res.inPost?.organizationId ?? '',
+ shipXBaseUrl: res.inPost?.shipXBaseUrl ?? '',
+ geowidgetPublicToken: '',
+ geowidgetSandboxToken: '',
+ geowidgetConfig: res.inPost?.geowidgetConfig ?? 'parcelCollect',
+ geowidgetSdkBaseUrl: res.inPost?.geowidgetSdkBaseUrl ?? '',
+ hasShipXToken: res.inPost?.hasShipXToken ?? false,
+ hasGeowidgetPublicToken: res.inPost?.hasGeowidgetPublicToken ?? false,
+ hasGeowidgetSandboxToken:
+ res.inPost?.hasGeowidgetSandboxToken ?? false,
+ },
+ dhl: {
+ apiKey: '',
+ apiSecret: '',
+ apiBaseUrl: res.dhl?.apiBaseUrl ?? '',
+ widgetUrl: res.dhl?.widgetUrl ?? 'https://parcelshop.dhl.pl/mapa',
+ hasApiKey: res.dhl?.hasApiKey ?? false,
+ hasApiSecret: res.dhl?.hasApiSecret ?? false,
+ },
+ dpd: {
+ login: res.dpd?.login ?? '',
+ password: '',
+ fid: res.dpd?.fid ?? '',
+ apiBaseUrl: res.dpd?.apiBaseUrl ?? '',
+ widgetUrl: res.dpd?.widgetUrl ?? 'https://pudofinder.dpd.com.pl/',
+ hasPassword: res.dpd?.hasPassword ?? false,
+ },
+ payU: {
+ useSandbox: res.payU?.useSandbox ?? true,
+ posId: res.payU?.posId ?? '',
+ clientId: res.payU?.clientId ?? '',
+ clientSecret: '',
+ secondKey: '',
+ oauthUrl: res.payU?.oauthUrl ?? '',
+ ordersUrl: res.payU?.ordersUrl ?? '',
+ notifyUrl: res.payU?.notifyUrl ?? '',
+ continueUrl: res.payU?.continueUrl ?? '',
+ hasClientSecret: res.payU?.hasClientSecret ?? false,
+ hasSecondKey: res.payU?.hasSecondKey ?? false,
+ },
};
}
@@ -196,6 +281,7 @@ export class SettingsAdminComponent implements OnInit {
locales: form.locales,
lowStockThreshold: form.lowStockThreshold,
carriers: form.carriers,
+ shippingUseSandbox: form.shippingUseSandbox,
emailFromName: form.emailFromName,
emailFromAddress: form.emailFromAddress,
emailReplyTo: form.emailReplyTo,
@@ -219,6 +305,39 @@ export class SettingsAdminComponent implements OnInit {
invoiceSeries: form.ifirma.invoiceSeries || null,
autoInvoice: form.ifirma.autoInvoice,
},
+ inPost: {
+ shipXToken: form.inPost.shipXToken || null,
+ organizationId: form.inPost.organizationId || null,
+ shipXBaseUrl: form.inPost.shipXBaseUrl || null,
+ geowidgetPublicToken: form.inPost.geowidgetPublicToken || null,
+ geowidgetSandboxToken: form.inPost.geowidgetSandboxToken || null,
+ geowidgetConfig: form.inPost.geowidgetConfig || null,
+ geowidgetSdkBaseUrl: form.inPost.geowidgetSdkBaseUrl || null,
+ },
+ dhl: {
+ apiKey: form.dhl.apiKey || null,
+ apiSecret: form.dhl.apiSecret || null,
+ apiBaseUrl: form.dhl.apiBaseUrl || null,
+ widgetUrl: form.dhl.widgetUrl || null,
+ },
+ dpd: {
+ login: form.dpd.login || null,
+ password: form.dpd.password || null,
+ fid: form.dpd.fid || null,
+ apiBaseUrl: form.dpd.apiBaseUrl || null,
+ widgetUrl: form.dpd.widgetUrl || null,
+ },
+ payU: {
+ useSandbox: form.payU.useSandbox,
+ posId: form.payU.posId || null,
+ clientId: form.payU.clientId || null,
+ clientSecret: form.payU.clientSecret || null,
+ secondKey: form.payU.secondKey || null,
+ oauthUrl: form.payU.oauthUrl || null,
+ ordersUrl: form.payU.ordersUrl || null,
+ notifyUrl: form.payU.notifyUrl || null,
+ continueUrl: form.payU.continueUrl || null,
+ },
};
}
}
diff --git a/src/admin/src/app/features/settings/settings.storage.ts b/src/admin/src/app/features/settings/settings.storage.ts
index 7829135..fa5e970 100644
--- a/src/admin/src/app/features/settings/settings.storage.ts
+++ b/src/admin/src/app/features/settings/settings.storage.ts
@@ -30,6 +30,51 @@ export interface IfirmaSettingsForm {
hasApiKey: boolean;
}
+export interface InPostSettingsForm {
+ shipXToken: string;
+ organizationId: string;
+ shipXBaseUrl: string;
+ geowidgetPublicToken: string;
+ geowidgetSandboxToken: string;
+ geowidgetConfig: string;
+ geowidgetSdkBaseUrl: string;
+ hasShipXToken: boolean;
+ hasGeowidgetPublicToken: boolean;
+ hasGeowidgetSandboxToken: boolean;
+}
+
+export interface DhlSettingsForm {
+ apiKey: string;
+ apiSecret: string;
+ apiBaseUrl: string;
+ widgetUrl: string;
+ hasApiKey: boolean;
+ hasApiSecret: boolean;
+}
+
+export interface DpdSettingsForm {
+ login: string;
+ password: string;
+ fid: string;
+ apiBaseUrl: string;
+ widgetUrl: string;
+ hasPassword: boolean;
+}
+
+export interface PayUSettingsForm {
+ useSandbox: boolean;
+ posId: string;
+ clientId: string;
+ clientSecret: string;
+ secondKey: string;
+ oauthUrl: string;
+ ordersUrl: string;
+ notifyUrl: string;
+ continueUrl: string;
+ hasClientSecret: boolean;
+ hasSecondKey: boolean;
+}
+
export interface AdminSettings {
storeName: string;
supportEmail: string;
@@ -40,12 +85,17 @@ export interface AdminSettings {
locales: { pl: boolean; en: boolean; de: boolean; cs: boolean };
lowStockThreshold: number;
carriers: { inPost: boolean; dhl: boolean; dpd: boolean };
+ shippingUseSandbox: boolean;
emailFromName: string;
emailFromAddress: string;
emailReplyTo: string;
allegro: AllegroSettingsForm;
amazon: AmazonSettingsForm;
ifirma: IfirmaSettingsForm;
+ inPost: InPostSettingsForm;
+ dhl: DhlSettingsForm;
+ dpd: DpdSettingsForm;
+ payU: PayUSettingsForm;
}
export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
@@ -58,6 +108,7 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
locales: { pl: true, en: true, de: true, cs: true },
lowStockThreshold: 5,
carriers: { inPost: true, dhl: true, dpd: true },
+ shippingUseSandbox: true,
emailFromName: 'iLED',
emailFromAddress: 'newsletter@iled.pl',
emailReplyTo: 'hello@iled.pl',
@@ -87,18 +138,54 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
autoInvoice: false,
hasApiKey: false,
},
+ inPost: {
+ shipXToken: '',
+ organizationId: '',
+ shipXBaseUrl: '',
+ geowidgetPublicToken: '',
+ geowidgetSandboxToken: '',
+ geowidgetConfig: 'parcelCollect',
+ geowidgetSdkBaseUrl: '',
+ hasShipXToken: false,
+ hasGeowidgetPublicToken: false,
+ hasGeowidgetSandboxToken: false,
+ },
+ dhl: {
+ apiKey: '',
+ apiSecret: '',
+ apiBaseUrl: '',
+ widgetUrl: 'https://parcelshop.dhl.pl/mapa',
+ hasApiKey: false,
+ hasApiSecret: false,
+ },
+ dpd: {
+ login: '',
+ password: '',
+ fid: '',
+ apiBaseUrl: '',
+ widgetUrl: 'https://pudofinder.dpd.com.pl/',
+ hasPassword: false,
+ },
+ payU: {
+ useSandbox: true,
+ posId: '',
+ clientId: '',
+ clientSecret: '',
+ secondKey: '',
+ oauthUrl: '',
+ ordersUrl: '',
+ notifyUrl: '',
+ continueUrl: '',
+ hasClientSecret: false,
+ hasSecondKey: false,
+ },
};
export function loadAdminSettings(): AdminSettings {
try {
const raw = localStorage.getItem(ADMIN_SETTINGS_KEY);
if (!raw) {
- return {
- ...DEFAULT_ADMIN_SETTINGS,
- allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
- amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
- ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
- };
+ return cloneDefaults();
}
const parsed = JSON.parse(raw) as Partial;
return {
@@ -109,14 +196,13 @@ export function loadAdminSettings(): AdminSettings {
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro, ...parsed.allegro },
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon, ...parsed.amazon },
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma, ...parsed.ifirma },
+ inPost: { ...DEFAULT_ADMIN_SETTINGS.inPost, ...parsed.inPost },
+ dhl: { ...DEFAULT_ADMIN_SETTINGS.dhl, ...parsed.dhl },
+ dpd: { ...DEFAULT_ADMIN_SETTINGS.dpd, ...parsed.dpd },
+ payU: { ...DEFAULT_ADMIN_SETTINGS.payU, ...parsed.payU },
};
} catch {
- return {
- ...DEFAULT_ADMIN_SETTINGS,
- allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
- amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
- ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
- };
+ return cloneDefaults();
}
}
@@ -127,3 +213,16 @@ export function saveAdminSettings(settings: AdminSettings): void {
String(settings.lowStockThreshold),
);
}
+
+function cloneDefaults(): AdminSettings {
+ return {
+ ...DEFAULT_ADMIN_SETTINGS,
+ allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
+ amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
+ ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
+ inPost: { ...DEFAULT_ADMIN_SETTINGS.inPost },
+ dhl: { ...DEFAULT_ADMIN_SETTINGS.dhl },
+ dpd: { ...DEFAULT_ADMIN_SETTINGS.dpd },
+ payU: { ...DEFAULT_ADMIN_SETTINGS.payU },
+ };
+}
diff --git a/src/admin/src/styles/admin-k.css b/src/admin/src/styles/admin-k.css
index 94c8407..72a524e 100644
--- a/src/admin/src/styles/admin-k.css
+++ b/src/admin/src/styles/admin-k.css
@@ -1775,21 +1775,42 @@ table.data tbody tr.is-selected {
gap: 6px;
}
+.meta-icon,
.channel-badge,
.delivery-badge {
display: inline-flex;
align-items: center;
- gap: 4px;
- padding: 2px 8px;
- border-radius: 999px;
+ justify-content: center;
+ gap: 6px;
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ border-radius: 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
- line-height: 1.4;
+ line-height: 1;
border: 1px solid var(--border);
background: var(--surface-muted);
color: var(--text-secondary);
white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.order-meta-icons--with-labels .meta-icon,
+.order-meta-icons--with-labels .channel-badge,
+.order-meta-icons--with-labels .delivery-badge {
+ width: auto;
+ height: auto;
+ min-height: 28px;
+ padding: 4px 10px 4px 8px;
+ border-radius: 999px;
+}
+
+.meta-icon__label {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.2;
}
.channel-badge[data-channel='shop'] {
diff --git a/src/backend/src/iLED.Api/Controllers/AdminController.cs b/src/backend/src/iLED.Api/Controllers/AdminController.cs
index e9866f3..e294c01 100644
--- a/src/backend/src/iLED.Api/Controllers/AdminController.cs
+++ b/src/backend/src/iLED.Api/Controllers/AdminController.cs
@@ -48,6 +48,7 @@ public sealed class AdminProductsController(IMediator mediator) : ControllerBase
[HttpPost("images/upload")]
[RequestSizeLimit(10 * 1024 * 1024)]
+ [RequestFormLimits(MultipartBodyLengthLimit = 10 * 1024 * 1024)]
public async Task UploadImage(
IFormFile file,
CancellationToken cancellationToken)
diff --git a/src/backend/src/iLED.Api/Program.cs b/src/backend/src/iLED.Api/Program.cs
index 43bd02e..d8bba9e 100644
--- a/src/backend/src/iLED.Api/Program.cs
+++ b/src/backend/src/iLED.Api/Program.cs
@@ -47,6 +47,14 @@ builder.Services.AddAuthentication(options =>
});
builder.Services.AddAuthorization();
+builder.Services.Configure(options =>
+{
+ options.MultipartBodyLengthLimit = 10 * 1024 * 1024;
+});
+builder.WebHost.ConfigureKestrel(options =>
+{
+ options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
+});
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
diff --git a/src/backend/src/iLED.Application/Admin/AnalyticsQueries.cs b/src/backend/src/iLED.Application/Admin/AnalyticsQueries.cs
index 0aad51d..a22ebb9 100644
--- a/src/backend/src/iLED.Application/Admin/AnalyticsQueries.cs
+++ b/src/backend/src/iLED.Application/Admin/AnalyticsQueries.cs
@@ -58,79 +58,87 @@ public sealed class GetAnalyticsQueryHandler(IAppDbContext db)
var start = end.AddDays(-days);
var prevStart = start.AddDays(-days);
- var orders = db.Orders
+ var baseQuery = db.Orders
.AsNoTracking()
.Where(o => !Excluded.Contains(o.Status));
if (request.Source.HasValue)
{
- orders = orders.Where(o => o.Source == request.Source.Value);
+ baseQuery = baseQuery.Where(o => o.Source == request.Source.Value);
}
- var current = await AggregateAsync(
- orders.Where(o => o.CreatedAt >= start && o.CreatedAt < end),
- cancellationToken);
-
- var previous = await AggregateAsync(
- orders.Where(o => o.CreatedAt >= prevStart && o.CreatedAt < start),
- cancellationToken);
-
- var trendRaw = await orders
+ var currentRows = await baseQuery
.Where(o => o.CreatedAt >= start && o.CreatedAt < end)
- .GroupBy(o => o.CreatedAt.Date)
- .Select(g => new
- {
- Date = g.Key,
- Revenue = g.Sum(o => o.Total),
- Orders = g.Count(),
- })
+ .Select(o => new OrderAggRow(
+ o.Id,
+ o.CreatedAt,
+ o.Total,
+ o.Source))
.ToListAsync(cancellationToken);
- var trendLookup = trendRaw.ToDictionary(x => x.Date);
+ var previousRows = await baseQuery
+ .Where(o => o.CreatedAt >= prevStart && o.CreatedAt < start)
+ .Select(o => new OrderAggRow(
+ o.Id,
+ o.CreatedAt,
+ o.Total,
+ o.Source))
+ .ToListAsync(cancellationToken);
+
+ var currentOrderIds = currentRows.Select(r => r.Id).ToList();
+ var previousOrderIds = previousRows.Select(r => r.Id).ToList();
+
+ var currentUnits = await SumUnitsAsync(
+ currentOrderIds,
+ cancellationToken);
+ var previousUnits = await SumUnitsAsync(
+ previousOrderIds,
+ cancellationToken);
+
+ var current = ToKpi(currentRows, currentUnits);
+ var previous = ToKpi(previousRows, previousUnits);
+
+ // Group by calendar day in-memory — Npgsql cannot reliably
+ // translate CreatedAt.Date for timestamp with time zone.
+ var trendLookup = currentRows
+ .GroupBy(r => DateOnly.FromDateTime(r.CreatedAt.ToUniversalTime()))
+ .ToDictionary(
+ g => g.Key,
+ g => (Revenue: g.Sum(x => x.Total), Orders: g.Count()));
+
var trend = new List(days);
- for (var d = start; d < end; d = d.AddDays(1))
+ for (var d = DateOnly.FromDateTime(start); d < DateOnly.FromDateTime(end); d = d.AddDays(1))
{
trendLookup.TryGetValue(d, out var row);
trend.Add(new AnalyticsTrendPointDto(
d.ToString("yyyy-MM-dd"),
- row?.Revenue ?? 0m,
- row?.Orders ?? 0));
+ row.Revenue,
+ row.Orders));
}
- var paidOrderIds = await orders
- .Where(o => o.CreatedAt >= start && o.CreatedAt < end)
- .Select(o => o.Id)
- .ToListAsync(cancellationToken);
-
- var topRaw = await db.Orders
- .AsNoTracking()
- .Where(o => paidOrderIds.Contains(o.Id))
- .SelectMany(o => o.Lines)
- .GroupBy(l => new { l.ProductVariantId, l.ProductName, l.Sku })
- .Select(g => new
- {
- g.Key.ProductName,
- g.Key.Sku,
- Units = g.Sum(l => l.Quantity),
- Revenue = g.Sum(l => l.UnitPrice * l.Quantity),
- VariantId = g.Key.ProductVariantId,
- })
- .OrderByDescending(x => x.Revenue)
- .Take(10)
- .ToListAsync(cancellationToken);
-
- var variantIds = topRaw.Select(t => t.VariantId).ToList();
- var productIds = await db.ProductVariants
- .AsNoTracking()
- .Where(v => variantIds.Contains(v.Id))
- .Select(v => new { v.Id, v.ProductId })
- .ToListAsync(cancellationToken);
- var productLookup = productIds.ToDictionary(v => v.Id, v => v.ProductId);
+ var topRaw = currentOrderIds.Count == 0
+ ? []
+ : await db.Orders
+ .AsNoTracking()
+ .Where(o => currentOrderIds.Contains(o.Id))
+ .SelectMany(o => o.Lines)
+ .GroupBy(l => new { l.ProductId, l.ProductName, l.Sku })
+ .Select(g => new
+ {
+ g.Key.ProductId,
+ g.Key.ProductName,
+ g.Key.Sku,
+ Units = g.Sum(l => l.Quantity),
+ Revenue = g.Sum(l => l.UnitPrice * l.Quantity),
+ })
+ .OrderByDescending(x => x.Revenue)
+ .Take(10)
+ .ToListAsync(cancellationToken);
var totalRevenue = current.Revenue == 0 ? 1m : current.Revenue;
var topProducts = topRaw
.Select(t => new AnalyticsTopProductDto(
- productLookup.GetValueOrDefault(t.VariantId),
+ t.ProductId,
t.ProductName,
t.Sku,
t.Units,
@@ -138,15 +146,14 @@ public sealed class GetAnalyticsQueryHandler(IAppDbContext db)
Math.Round(t.Revenue / totalRevenue * 100m, 1)))
.ToList();
- var byChannel = await orders
- .Where(o => o.CreatedAt >= start && o.CreatedAt < end)
- .GroupBy(o => o.Source)
+ var byChannel = currentRows
+ .GroupBy(r => r.Source)
.Select(g => new AnalyticsChannelBreakdownDto(
g.Key,
g.Count(),
- g.Sum(o => o.Total)))
+ g.Sum(x => x.Total)))
.OrderByDescending(x => x.Revenue)
- .ToListAsync(cancellationToken);
+ .ToList();
return new AnalyticsDto(
current,
@@ -156,20 +163,35 @@ public sealed class GetAnalyticsQueryHandler(IAppDbContext db)
byChannel);
}
- private static async Task AggregateAsync(
- IQueryable query,
+ private async Task SumUnitsAsync(
+ IReadOnlyList orderIds,
CancellationToken cancellationToken)
{
- var orders = await query.CountAsync(cancellationToken);
- var revenue = orders == 0
- ? 0m
- : await query.SumAsync(o => o.Total, cancellationToken);
- var units = orders == 0
- ? 0
- : await query.SelectMany(o => o.Lines)
- .SumAsync(l => l.Quantity, cancellationToken);
- var aov = orders == 0 ? 0m : Math.Round(revenue / orders, 2);
+ if (orderIds.Count == 0)
+ {
+ return 0;
+ }
+ return await db.Orders
+ .AsNoTracking()
+ .Where(o => orderIds.Contains(o.Id))
+ .SelectMany(o => o.Lines)
+ .SumAsync(l => (int?)l.Quantity, cancellationToken) ?? 0;
+ }
+
+ private static AnalyticsKpiDto ToKpi(
+ IReadOnlyList rows,
+ int units)
+ {
+ var orders = rows.Count;
+ var revenue = orders == 0 ? 0m : rows.Sum(r => r.Total);
+ var aov = orders == 0 ? 0m : Math.Round(revenue / orders, 2);
return new AnalyticsKpiDto(revenue, orders, aov, units);
}
+
+ private sealed record OrderAggRow(
+ Guid Id,
+ DateTime CreatedAt,
+ decimal Total,
+ OrderSource Source);
}
diff --git a/src/backend/src/iLED.Application/Admin/SettingsCommands.cs b/src/backend/src/iLED.Application/Admin/SettingsCommands.cs
index 47b4b18..b6d436f 100644
--- a/src/backend/src/iLED.Application/Admin/SettingsCommands.cs
+++ b/src/backend/src/iLED.Application/Admin/SettingsCommands.cs
@@ -20,7 +20,12 @@ public sealed record StoreSettingsDto(
string EmailReplyTo,
AllegroSettingsDto Allegro,
AmazonSettingsDto Amazon,
- IfirmaSettingsDto Ifirma);
+ IfirmaSettingsDto Ifirma,
+ InPostSettingsDto InPost,
+ DhlSettingsDto Dhl,
+ DpdSettingsDto Dpd,
+ PayUSettingsDto PayU,
+ bool ShippingUseSandbox);
public sealed record LocaleFlagsDto(bool Pl, bool En, bool De, bool Cs);
@@ -48,6 +53,39 @@ public sealed record IfirmaSettingsDto(
string? InvoiceSeries,
bool AutoInvoice);
+public sealed record InPostSettingsDto(
+ bool HasShipXToken,
+ string? OrganizationId,
+ string? ShipXBaseUrl,
+ bool HasGeowidgetPublicToken,
+ bool HasGeowidgetSandboxToken,
+ string? GeowidgetConfig,
+ string? GeowidgetSdkBaseUrl);
+
+public sealed record DhlSettingsDto(
+ bool HasApiKey,
+ bool HasApiSecret,
+ string? ApiBaseUrl,
+ string? WidgetUrl);
+
+public sealed record DpdSettingsDto(
+ string? Login,
+ bool HasPassword,
+ string? Fid,
+ string? ApiBaseUrl,
+ string? WidgetUrl);
+
+public sealed record PayUSettingsDto(
+ bool UseSandbox,
+ string? PosId,
+ string? ClientId,
+ bool HasClientSecret,
+ bool HasSecondKey,
+ string? OauthUrl,
+ string? OrdersUrl,
+ string? NotifyUrl,
+ string? ContinueUrl);
+
public sealed record GetStoreSettingsQuery() : IRequest;
public sealed class GetStoreSettingsQueryHandler(IAppDbContext db)
@@ -98,7 +136,37 @@ public sealed class GetStoreSettingsQueryHandler(IAppDbContext db)
s.IfirmaLogin,
!string.IsNullOrEmpty(s.IfirmaApiKey),
s.IfirmaInvoiceSeries,
- s.IfirmaAutoInvoice));
+ s.IfirmaAutoInvoice),
+ new InPostSettingsDto(
+ !string.IsNullOrEmpty(s.InPostShipXToken),
+ s.InPostOrganizationId,
+ s.InPostShipXBaseUrl,
+ !string.IsNullOrEmpty(s.InPostGeowidgetPublicToken),
+ !string.IsNullOrEmpty(s.InPostGeowidgetSandboxToken),
+ s.InPostGeowidgetConfig,
+ s.InPostGeowidgetSdkBaseUrl),
+ new DhlSettingsDto(
+ !string.IsNullOrEmpty(s.DhlApiKey),
+ !string.IsNullOrEmpty(s.DhlApiSecret),
+ s.DhlApiBaseUrl,
+ s.DhlWidgetUrl),
+ new DpdSettingsDto(
+ s.DpdLogin,
+ !string.IsNullOrEmpty(s.DpdPassword),
+ s.DpdFid,
+ s.DpdApiBaseUrl,
+ s.DpdWidgetUrl),
+ new PayUSettingsDto(
+ s.PayUUseSandbox,
+ s.PayUPosId,
+ s.PayUClientId,
+ !string.IsNullOrEmpty(s.PayUClientSecret),
+ !string.IsNullOrEmpty(s.PayUSecondKey),
+ s.PayUOauthUrl,
+ s.PayUOrdersUrl,
+ s.PayUNotifyUrl,
+ s.PayUContinueUrl),
+ s.ShippingUseSandbox);
}
public sealed record UpdateStoreSettingsCommand(
@@ -116,7 +184,12 @@ public sealed record UpdateStoreSettingsCommand(
string EmailReplyTo,
UpdateAllegroSettingsDto? Allegro,
UpdateAmazonSettingsDto? Amazon,
- UpdateIfirmaSettingsDto? Ifirma) : IRequest;
+ UpdateIfirmaSettingsDto? Ifirma,
+ UpdateInPostSettingsDto? InPost,
+ UpdateDhlSettingsDto? Dhl,
+ UpdateDpdSettingsDto? Dpd,
+ UpdatePayUSettingsDto? PayU,
+ bool ShippingUseSandbox) : IRequest;
public sealed record UpdateAllegroSettingsDto(
bool Enabled,
@@ -138,6 +211,39 @@ public sealed record UpdateIfirmaSettingsDto(
string? InvoiceSeries,
bool AutoInvoice);
+public sealed record UpdateInPostSettingsDto(
+ string? ShipXToken,
+ string? OrganizationId,
+ string? ShipXBaseUrl,
+ string? GeowidgetPublicToken,
+ string? GeowidgetSandboxToken,
+ string? GeowidgetConfig,
+ string? GeowidgetSdkBaseUrl);
+
+public sealed record UpdateDhlSettingsDto(
+ string? ApiKey,
+ string? ApiSecret,
+ string? ApiBaseUrl,
+ string? WidgetUrl);
+
+public sealed record UpdateDpdSettingsDto(
+ string? Login,
+ string? Password,
+ string? Fid,
+ string? ApiBaseUrl,
+ string? WidgetUrl);
+
+public sealed record UpdatePayUSettingsDto(
+ bool UseSandbox,
+ string? PosId,
+ string? ClientId,
+ string? ClientSecret,
+ string? SecondKey,
+ string? OauthUrl,
+ string? OrdersUrl,
+ string? NotifyUrl,
+ string? ContinueUrl);
+
public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
: IRequestHandler
{
@@ -175,16 +281,14 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
row.EmailFromName = request.EmailFromName.Trim();
row.EmailFromAddress = request.EmailFromAddress.Trim();
row.EmailReplyTo = request.EmailReplyTo.Trim();
+ row.ShippingUseSandbox = request.ShippingUseSandbox;
if (request.Allegro is not null)
{
row.AllegroEnabled = request.Allegro.Enabled;
row.AllegroClientId = TrimOrNull(request.Allegro.ClientId);
row.AllegroSellerId = TrimOrNull(request.Allegro.SellerId);
- if (!string.IsNullOrWhiteSpace(request.Allegro.ClientSecret))
- {
- row.AllegroClientSecret = request.Allegro.ClientSecret.Trim();
- }
+ SetSecret(request.Allegro.ClientSecret, v => row.AllegroClientSecret = v);
}
if (request.Amazon is not null)
@@ -192,15 +296,8 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
row.AmazonEnabled = request.Amazon.Enabled;
row.AmazonSellerId = TrimOrNull(request.Amazon.SellerId);
row.AmazonMarketplaceId = TrimOrNull(request.Amazon.MarketplaceId);
- if (!string.IsNullOrWhiteSpace(request.Amazon.AccessKey))
- {
- row.AmazonAccessKey = request.Amazon.AccessKey.Trim();
- }
-
- if (!string.IsNullOrWhiteSpace(request.Amazon.SecretKey))
- {
- row.AmazonSecretKey = request.Amazon.SecretKey.Trim();
- }
+ SetSecret(request.Amazon.AccessKey, v => row.AmazonAccessKey = v);
+ SetSecret(request.Amazon.SecretKey, v => row.AmazonSecretKey = v);
}
if (request.Ifirma is not null)
@@ -209,10 +306,52 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
row.IfirmaLogin = TrimOrNull(request.Ifirma.Login);
row.IfirmaInvoiceSeries = TrimOrNull(request.Ifirma.InvoiceSeries);
row.IfirmaAutoInvoice = request.Ifirma.AutoInvoice;
- if (!string.IsNullOrWhiteSpace(request.Ifirma.ApiKey))
- {
- row.IfirmaApiKey = request.Ifirma.ApiKey.Trim();
- }
+ SetSecret(request.Ifirma.ApiKey, v => row.IfirmaApiKey = v);
+ }
+
+ if (request.InPost is not null)
+ {
+ row.InPostOrganizationId = TrimOrNull(request.InPost.OrganizationId);
+ row.InPostShipXBaseUrl = TrimOrNull(request.InPost.ShipXBaseUrl);
+ row.InPostGeowidgetConfig = TrimOrNull(request.InPost.GeowidgetConfig);
+ row.InPostGeowidgetSdkBaseUrl = TrimOrNull(request.InPost.GeowidgetSdkBaseUrl);
+ SetSecret(request.InPost.ShipXToken, v => row.InPostShipXToken = v);
+ SetSecret(
+ request.InPost.GeowidgetPublicToken,
+ v => row.InPostGeowidgetPublicToken = v);
+ SetSecret(
+ request.InPost.GeowidgetSandboxToken,
+ v => row.InPostGeowidgetSandboxToken = v);
+ }
+
+ if (request.Dhl is not null)
+ {
+ row.DhlApiBaseUrl = TrimOrNull(request.Dhl.ApiBaseUrl);
+ row.DhlWidgetUrl = TrimOrNull(request.Dhl.WidgetUrl);
+ SetSecret(request.Dhl.ApiKey, v => row.DhlApiKey = v);
+ SetSecret(request.Dhl.ApiSecret, v => row.DhlApiSecret = v);
+ }
+
+ if (request.Dpd is not null)
+ {
+ row.DpdLogin = TrimOrNull(request.Dpd.Login);
+ row.DpdFid = TrimOrNull(request.Dpd.Fid);
+ row.DpdApiBaseUrl = TrimOrNull(request.Dpd.ApiBaseUrl);
+ row.DpdWidgetUrl = TrimOrNull(request.Dpd.WidgetUrl);
+ SetSecret(request.Dpd.Password, v => row.DpdPassword = v);
+ }
+
+ if (request.PayU is not null)
+ {
+ row.PayUUseSandbox = request.PayU.UseSandbox;
+ row.PayUPosId = TrimOrNull(request.PayU.PosId);
+ row.PayUClientId = TrimOrNull(request.PayU.ClientId);
+ row.PayUOauthUrl = TrimOrNull(request.PayU.OauthUrl);
+ row.PayUOrdersUrl = TrimOrNull(request.PayU.OrdersUrl);
+ row.PayUNotifyUrl = TrimOrNull(request.PayU.NotifyUrl);
+ row.PayUContinueUrl = TrimOrNull(request.PayU.ContinueUrl);
+ SetSecret(request.PayU.ClientSecret, v => row.PayUClientSecret = v);
+ SetSecret(request.PayU.SecondKey, v => row.PayUSecondKey = v);
}
row.UpdatedAt = DateTime.UtcNow;
@@ -244,4 +383,12 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
private static string? TrimOrNull(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+
+ private static void SetSecret(string? incoming, Action assign)
+ {
+ if (!string.IsNullOrWhiteSpace(incoming))
+ {
+ assign(incoming.Trim());
+ }
+ }
}
diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IIntegrationSettingsAccessor.cs b/src/backend/src/iLED.Application/Common/Interfaces/IIntegrationSettingsAccessor.cs
new file mode 100644
index 0000000..1f95ccd
--- /dev/null
+++ b/src/backend/src/iLED.Application/Common/Interfaces/IIntegrationSettingsAccessor.cs
@@ -0,0 +1,72 @@
+namespace iLED.Application.Common.Interfaces;
+
+///
+/// Runtime PayU + carrier credentials from StoreSettings,
+/// falling back to appsettings when a DB field is empty.
+///
+public interface IIntegrationSettingsAccessor
+{
+ Task GetAsync(
+ CancellationToken cancellationToken = default);
+}
+
+public sealed class IntegrationRuntimeSettings
+{
+ public bool ShippingUseSandbox { get; init; } = true;
+
+ public bool CarrierInPost { get; init; } = true;
+
+ public bool CarrierDhl { get; init; } = true;
+
+ public bool CarrierDpd { get; init; } = true;
+
+ public string? InPostShipXToken { get; init; }
+
+ public string? InPostOrganizationId { get; init; }
+
+ public string? InPostShipXBaseUrl { get; init; }
+
+ public string? InPostGeowidgetPublicToken { get; init; }
+
+ public string? InPostGeowidgetSandboxToken { get; init; }
+
+ public string? InPostGeowidgetConfig { get; init; }
+
+ public string? InPostGeowidgetSdkBaseUrl { get; init; }
+
+ public string? DhlApiKey { get; init; }
+
+ public string? DhlApiSecret { get; init; }
+
+ public string? DhlApiBaseUrl { get; init; }
+
+ public string? DhlWidgetUrl { get; init; }
+
+ public string? DpdLogin { get; init; }
+
+ public string? DpdPassword { get; init; }
+
+ public string? DpdFid { get; init; }
+
+ public string? DpdApiBaseUrl { get; init; }
+
+ public string? DpdWidgetUrl { get; init; }
+
+ public bool PayUUseSandbox { get; init; } = true;
+
+ public string? PayUPosId { get; init; }
+
+ public string? PayUClientId { get; init; }
+
+ public string? PayUClientSecret { get; init; }
+
+ public string? PayUSecondKey { get; init; }
+
+ public string? PayUOauthUrl { get; init; }
+
+ public string? PayUOrdersUrl { get; init; }
+
+ public string? PayUNotifyUrl { get; init; }
+
+ public string? PayUContinueUrl { get; init; }
+}
diff --git a/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs b/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs
index 46abf77..116b2d0 100644
--- a/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs
+++ b/src/backend/src/iLED.Application/Common/Interfaces/IShippingService.cs
@@ -12,7 +12,8 @@ public interface IShippingService
ShipmentRequest request,
CancellationToken cancellationToken = default);
- ShippingWidgetConfigDto GetWidgetConfig();
+ Task GetWidgetConfigAsync(
+ CancellationToken cancellationToken = default);
}
public sealed record ShippingParcelPointSearch(
diff --git a/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs b/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs
index 0a96168..98e8637 100644
--- a/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs
+++ b/src/backend/src/iLED.Application/Payments/Commands/PaymentCommands.cs
@@ -17,7 +17,8 @@ public sealed class InitiatePaymentCommandHandler(
IOrderService orderService,
IPaymentService paymentService,
IAppDbContext db,
- IConfiguration configuration)
+ IConfiguration configuration,
+ IIntegrationSettingsAccessor integrationSettings)
: IRequestHandler
{
public async Task Handle(
@@ -27,10 +28,11 @@ public sealed class InitiatePaymentCommandHandler(
var order = await orderService.GetOrderAsync(
request.OrderId, cancellationToken);
+ var settings = await integrationSettings.GetAsync(cancellationToken);
var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200";
- var notifyUrl = configuration["PayU:NotifyUrl"]
+ var notifyUrl = settings.PayUNotifyUrl
?? "http://localhost:5000/api/v1/payments/webhook";
- var continueBase = configuration["PayU:ContinueUrl"] ?? $"{shopBase}/order";
+ var continueBase = settings.PayUContinueUrl ?? $"{shopBase}/order";
var paymentRequest = new PaymentInitiationRequest(
order.Id,
@@ -69,14 +71,15 @@ public sealed record CompleteSandboxPaymentCommand(Guid OrderId) : IRequest
{
public async Task Handle(
CompleteSandboxPaymentCommand request,
CancellationToken cancellationToken)
{
- if (!configuration.GetValue("PayU:UseSandbox", true))
+ var settings = await integrationSettings.GetAsync(cancellationToken);
+ if (!settings.PayUUseSandbox)
{
return new PaymentResultDto(
false,
diff --git a/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs b/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs
index 7aaf2ba..7b42c52 100644
--- a/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs
+++ b/src/backend/src/iLED.Application/Shipping/Commands/ShippingCommands.cs
@@ -37,7 +37,7 @@ public sealed class GetShippingWidgetConfigQueryHandler(IShippingService shippin
public Task Handle(
GetShippingWidgetConfigQuery request,
CancellationToken cancellationToken) =>
- Task.FromResult(shipping.GetWidgetConfig());
+ shipping.GetWidgetConfigAsync(cancellationToken);
}
public sealed record CreateShipmentLabelCommand(Guid OrderId)
diff --git a/src/backend/src/iLED.Domain/Admin/StoreSettings.cs b/src/backend/src/iLED.Domain/Admin/StoreSettings.cs
index 6904f6e..8ea2321 100644
--- a/src/backend/src/iLED.Domain/Admin/StoreSettings.cs
+++ b/src/backend/src/iLED.Domain/Admin/StoreSettings.cs
@@ -73,4 +73,58 @@ public class StoreSettings : BaseEntity
public string? IfirmaInvoiceSeries { get; set; }
public bool IfirmaAutoInvoice { get; set; }
+
+ // ── Shipping credentials ─────────────────────────────────
+ public bool ShippingUseSandbox { get; set; } = true;
+
+ public string? InPostShipXToken { get; set; }
+
+ public string? InPostOrganizationId { get; set; }
+
+ public string? InPostShipXBaseUrl { get; set; }
+
+ public string? InPostGeowidgetPublicToken { get; set; }
+
+ public string? InPostGeowidgetSandboxToken { get; set; }
+
+ public string? InPostGeowidgetConfig { get; set; }
+
+ public string? InPostGeowidgetSdkBaseUrl { get; set; }
+
+ public string? DhlApiKey { get; set; }
+
+ public string? DhlApiSecret { get; set; }
+
+ public string? DhlApiBaseUrl { get; set; }
+
+ public string? DhlWidgetUrl { get; set; }
+
+ public string? DpdLogin { get; set; }
+
+ public string? DpdPassword { get; set; }
+
+ public string? DpdFid { get; set; }
+
+ public string? DpdApiBaseUrl { get; set; }
+
+ public string? DpdWidgetUrl { get; set; }
+
+ // ── PayU ─────────────────────────────────────────────────
+ public bool PayUUseSandbox { get; set; } = true;
+
+ public string? PayUPosId { get; set; }
+
+ public string? PayUClientId { get; set; }
+
+ public string? PayUClientSecret { get; set; }
+
+ public string? PayUSecondKey { get; set; }
+
+ public string? PayUOauthUrl { get; set; }
+
+ public string? PayUOrdersUrl { get; set; }
+
+ public string? PayUNotifyUrl { get; set; }
+
+ public string? PayUContinueUrl { get; set; }
}
diff --git a/src/backend/src/iLED.Infrastructure/DependencyInjection.cs b/src/backend/src/iLED.Infrastructure/DependencyInjection.cs
index 96a4de8..10cfaa8 100644
--- a/src/backend/src/iLED.Infrastructure/DependencyInjection.cs
+++ b/src/backend/src/iLED.Infrastructure/DependencyInjection.cs
@@ -56,6 +56,7 @@ public static class DependencyInjection
{
client.Timeout = TimeSpan.FromSeconds(60);
});
+ services.AddScoped();
services.AddScoped();
services.AddHostedService();
services.AddScoped();
diff --git a/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
index ef08279..b0074b6 100644
--- a/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/src/backend/src/iLED.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -213,6 +213,33 @@ namespace iLED.Infrastructure.Migrations
.IsRequired()
.HasColumnType("text");
+ b.Property("DhlApiBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("DhlApiKey")
+ .HasColumnType("text");
+
+ b.Property("DhlApiSecret")
+ .HasColumnType("text");
+
+ b.Property("DhlWidgetUrl")
+ .HasColumnType("text");
+
+ b.Property("DpdApiBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("DpdFid")
+ .HasColumnType("text");
+
+ b.Property("DpdLogin")
+ .HasColumnType("text");
+
+ b.Property("DpdPassword")
+ .HasColumnType("text");
+
+ b.Property("DpdWidgetUrl")
+ .HasColumnType("text");
+
b.Property("EmailFromAddress")
.IsRequired()
.HasColumnType("text");
@@ -240,6 +267,27 @@ namespace iLED.Infrastructure.Migrations
b.Property("IfirmaLogin")
.HasColumnType("text");
+ b.Property("InPostGeowidgetConfig")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetPublicToken")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetSandboxToken")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetSdkBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("InPostOrganizationId")
+ .HasColumnType("text");
+
+ b.Property("InPostShipXBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("InPostShipXToken")
+ .HasColumnType("text");
+
b.Property("LegalName")
.IsRequired()
.HasColumnType("text");
@@ -259,6 +307,36 @@ namespace iLED.Infrastructure.Migrations
b.Property("LowStockThreshold")
.HasColumnType("integer");
+ b.Property("PayUClientId")
+ .HasColumnType("text");
+
+ b.Property("PayUClientSecret")
+ .HasColumnType("text");
+
+ b.Property("PayUContinueUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUNotifyUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUOauthUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUOrdersUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUPosId")
+ .HasColumnType("text");
+
+ b.Property("PayUSecondKey")
+ .HasColumnType("text");
+
+ b.Property("PayUUseSandbox")
+ .HasColumnType("boolean");
+
+ b.Property("ShippingUseSandbox")
+ .HasColumnType("boolean");
+
b.Property("StoreName")
.IsRequired()
.HasColumnType("text");
diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261019130010_ShippingPayUIntegrations.Designer.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261019130010_ShippingPayUIntegrations.Designer.cs
new file mode 100644
index 0000000..30a97b5
--- /dev/null
+++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261019130010_ShippingPayUIntegrations.Designer.cs
@@ -0,0 +1,2229 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using iLED.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace iLED.Infrastructure.Persistence.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20261019130010_ShippingPayUIntegrations")]
+ partial class ShippingPayUIntegrations
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("iLED.Domain.Admin.StoreSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllegroClientId")
+ .HasColumnType("text");
+
+ b.Property("AllegroClientSecret")
+ .HasColumnType("text");
+
+ b.Property("AllegroEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("AllegroLastSyncAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("AllegroSellerId")
+ .HasColumnType("text");
+
+ b.Property("AmazonAccessKey")
+ .HasColumnType("text");
+
+ b.Property("AmazonEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("AmazonLastSyncAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("AmazonMarketplaceId")
+ .HasColumnType("text");
+
+ b.Property("AmazonSecretKey")
+ .HasColumnType("text");
+
+ b.Property("AmazonSellerId")
+ .HasColumnType("text");
+
+ b.Property("CarrierDhl")
+ .HasColumnType("boolean");
+
+ b.Property("CarrierDpd")
+ .HasColumnType("boolean");
+
+ b.Property("CarrierInPost")
+ .HasColumnType("boolean");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Currency")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DefaultLocale")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DhlApiBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("DhlApiKey")
+ .HasColumnType("text");
+
+ b.Property("DhlApiSecret")
+ .HasColumnType("text");
+
+ b.Property("DhlWidgetUrl")
+ .HasColumnType("text");
+
+ b.Property("DpdApiBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("DpdFid")
+ .HasColumnType("text");
+
+ b.Property("DpdLogin")
+ .HasColumnType("text");
+
+ b.Property("DpdPassword")
+ .HasColumnType("text");
+
+ b.Property("DpdWidgetUrl")
+ .HasColumnType("text");
+
+ b.Property("EmailFromAddress")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("EmailFromName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("EmailReplyTo")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IfirmaApiKey")
+ .HasColumnType("text");
+
+ b.Property("IfirmaAutoInvoice")
+ .HasColumnType("boolean");
+
+ b.Property("IfirmaEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("IfirmaInvoiceSeries")
+ .HasColumnType("text");
+
+ b.Property("IfirmaLogin")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetConfig")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetPublicToken")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetSandboxToken")
+ .HasColumnType("text");
+
+ b.Property("InPostGeowidgetSdkBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("InPostOrganizationId")
+ .HasColumnType("text");
+
+ b.Property("InPostShipXBaseUrl")
+ .HasColumnType("text");
+
+ b.Property("InPostShipXToken")
+ .HasColumnType("text");
+
+ b.Property("LegalName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("LocaleCs")
+ .HasColumnType("boolean");
+
+ b.Property("LocaleDe")
+ .HasColumnType("boolean");
+
+ b.Property("LocaleEn")
+ .HasColumnType("boolean");
+
+ b.Property("LocalePl")
+ .HasColumnType("boolean");
+
+ b.Property("LowStockThreshold")
+ .HasColumnType("integer");
+
+ b.Property("PayUClientId")
+ .HasColumnType("text");
+
+ b.Property("PayUClientSecret")
+ .HasColumnType("text");
+
+ b.Property("PayUContinueUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUNotifyUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUOauthUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUOrdersUrl")
+ .HasColumnType("text");
+
+ b.Property("PayUPosId")
+ .HasColumnType("text");
+
+ b.Property("PayUSecondKey")
+ .HasColumnType("text");
+
+ b.Property("PayUUseSandbox")
+ .HasColumnType("boolean");
+
+ b.Property("ShippingUseSandbox")
+ .HasColumnType("boolean");
+
+ b.Property("StoreName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("StorefrontUrl")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SupportEmail")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("StoreSettings");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Cart.Cart", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SessionId")
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Carts");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Cart.CartItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CartId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ProductId")
+ .HasColumnType("uuid");
+
+ b.Property("ProductVariantId")
+ .HasColumnType("uuid");
+
+ b.Property("Quantity")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CartId");
+
+ b.ToTable("CartItems");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ProductId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProductId");
+
+ b.ToTable("FavoriteItems");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.Category", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ParentCategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentCategoryId");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("Categories");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.Product", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BasePrice")
+ .HasColumnType("numeric");
+
+ b.Property("CategoryId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("DeliveryHours")
+ .HasColumnType("integer");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Ean")
+ .HasColumnType("text");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("IsBestseller")
+ .HasColumnType("boolean");
+
+ b.Property("IsNewArrival")
+ .HasColumnType("boolean");
+
+ b.Property("LowestPrice30Days")
+ .HasColumnType("numeric");
+
+ b.Property("Manufacturer")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Model")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ProductCode")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ProductGroupId")
+ .HasColumnType("uuid");
+
+ b.Property("ShortDescription")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoryId");
+
+ b.HasIndex("ProductGroupId");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("Products");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FilterGroup")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ProductId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProductId");
+
+ b.ToTable("ProductAttributes");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.ProductChannelPrice", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Channel")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Currency")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ExternalOfferId")
+ .HasColumnType("text");
+
+ b.Property("ExternalSku")
+ .HasColumnType("text");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Price")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)");
+
+ b.Property("ProductId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProductId", "Channel")
+ .IsUnique();
+
+ b.ToTable("ProductChannelPrices");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("ProductGroups");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AltText")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsPrimary")
+ .HasColumnType("boolean");
+
+ b.Property("ProductId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProductId");
+
+ b.ToTable("ProductImages");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ColorHex")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ColorName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsDefault")
+ .HasColumnType("boolean");
+
+ b.Property("PriceAdjustment")
+ .HasColumnType("numeric");
+
+ b.Property("ProductId")
+ .HasColumnType("uuid");
+
+ b.Property("Sku")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProductId");
+
+ b.HasIndex("Sku")
+ .IsUnique();
+
+ b.ToTable("ProductVariants");
+ });
+
+ modelBuilder.Entity("iLED.Domain.Catalog.Review", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AuthorEmail")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property |