diff --git a/docker/nginx-spa.conf b/docker/nginx-spa.conf index f787450..a25e74f 100644 --- a/docker/nginx-spa.conf +++ b/docker/nginx-spa.conf @@ -4,6 +4,11 @@ server { root /usr/share/nginx/html; index index.html; + # Product image uploads are up to 10 MB (API RequestSizeLimit). + # Nginx default is 1m and returns 413 for larger bodies. + client_max_body_size 12m; + client_body_timeout 120s; + # Browser calls same-origin /api and /uploads; proxy to API container. location /api/ { proxy_pass http://api:8080/api/; @@ -12,6 +17,10 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_request_buffering on; + proxy_connect_timeout 60s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; } location /uploads/ { diff --git a/src/admin/src/app/core/i18n/translations/cs.ts b/src/admin/src/app/core/i18n/translations/cs.ts index 4a98c7e..8c5013b 100644 --- a/src/admin/src/app/core/i18n/translations/cs.ts +++ b/src/admin/src/app/core/i18n/translations/cs.ts @@ -53,14 +53,40 @@ export const CS_TRANSLATIONS: TranslationMap = { 'dashboard.errorLoad': 'Nepodařilo se načíst dashboard', 'dashboard.alertLowStock': 'SKU s nízkým stavem', 'dashboard.alertPending': 'čeká na platbu', + 'dashboard.alertReviews': 'recenzí k moderaci', + 'dashboard.viewOrders': 'Zobrazit objednávky', + 'dashboard.openPickList': 'Otevřít pick list', + 'dashboard.pendingFulfillment': 'K vyřízení', + 'dashboard.pendingFulfillmentHint': 'Vyžaduje pozornost', + 'dashboard.lowStockHint': 'Práh ≤ 5', + 'dashboard.needsAction': 'Vyžaduje akci — objednávky', + 'dashboard.pendingReviews': 'Recenze k moderaci', + 'dashboard.moderate': 'Moderovat', + 'dashboard.restock': 'Doplnit', + 'dashboard.noActionOrders': 'Žádné objednávky nevyžadují akci.', + 'dashboard.noLowStock': 'Stavy skladu vypadají v pořádku.', + 'dashboard.noPendingReviews': 'Žádné recenze nečekají.', + 'orders.purpose': + 'Vyřízení objednávek — vyberte více pro kompletaci, faktury nebo štítky.', + 'orders.order': 'Objednávka', 'orders.bulkSelected': 'Vybráno {count}', 'orders.bulkPick': 'Kompletovat / pick', 'orders.bulkInvoices': 'Generovat faktury', 'orders.bulkLabels': 'Generovat štítky', 'orders.bulkLabelsSoon': 'Hromadné štítky vygenerují přepravní štítky pro výběr.', + 'orders.bulkLabelsEmpty': 'Nejdřív vyberte objednávky', + 'orders.bulkLabelsDone': + 'Štítky: {{ok}} vytvořeno, {{fail}} chyb, {{skip}} přeskočeno', + 'orders.bulkLabelsError': 'Hromadné generování štítků selhalo', 'orders.selectAll': 'Vybrat vše', 'orders.selectRow': 'Vybrat objednávku', 'products.title': 'Produkty', + 'products.purpose': 'Správa prodejního katalogu a skladů.', + 'promotions.purpose': 'Kampaně a promo kódy bez zmatku.', + 'newsletter.purpose': + 'Komunikace s odběrateli — sestavení, náhled, bezpečné odeslání.', + 'reviews.purpose': + 'Fronta moderace — ochrana důvěry na obchodě.', 'products.add': 'Přidat produkt', 'products.edit': 'Upravit', 'products.editTitle': 'Upravit produkt', @@ -237,6 +263,11 @@ export const CS_TRANSLATIONS: TranslationMap = { 'orders.markCollected': 'Označit jako sestavené', 'orders.collected': 'Sestaveno', 'orders.showCollected': 'Zobrazit sestavené', + 'orders.pickByOrder': 'Podle objednávek', + 'orders.pickByProduct': 'Podle produktů', + 'orders.pickTotalQty': 'Celkem k vyzvednutí: {{count}}', + 'orders.collectProduct': 'Kompletovat všechny', + 'orders.productCollected': 'Všechny kompletovány', 'orders.pendingItems': 'Čekající', 'orders.collectedItems': 'Sestavené', 'orders.orderWarehouse': 'Sklad pro objednávku', @@ -294,6 +325,7 @@ export const CS_TRANSLATIONS: TranslationMap = { 'promotions.kpiActive': 'Aktivní akce', 'promotions.kpiRedemptions': 'Uplatnění (stránka)', 'promotions.kpiCodes': 'Kódy (stránka)', + 'promotions.kpiDiscount': 'Poskytnuté slevy (30 dní)', 'promotions.sectionBenefit': 'Výhoda', 'promotions.sectionRequirements': 'Požadavky', 'promotions.sectionLimits': 'Limity použití', @@ -330,6 +362,7 @@ export const CS_TRANSLATIONS: TranslationMap = { 'newsletter.searchCampaign': 'Hledat kampaně…', 'newsletter.joined': 'Připojen', 'newsletter.sent': 'Odesláno', + 'newsletter.recipients': 'Příjemci', 'newsletter.unsubscribed': 'Odhlášen', 'newsletter.status.sent': 'Odesláno', 'newsletter.status.draft': 'Koncept', @@ -429,6 +462,8 @@ export const CS_TRANSLATIONS: TranslationMap = { 'settings.title': 'Nastavení', 'settings.purpose': 'Konfigurace obchodu — lokálně, dokud nebudou API nastavení.', + 'settings.errorLoad': 'Nepodařilo se načíst nastavení', + 'settings.errorSave': 'Nepodařilo se uložit nastavení', 'settings.save': 'Uložit změny', 'settings.navStore': 'Profil obchodu', 'settings.navLocales': 'Jazyky a měna', @@ -437,10 +472,36 @@ export const CS_TRANSLATIONS: TranslationMap = { 'settings.navIntegrations': 'Integrace', 'settings.navEmail': 'E-mail', 'settings.navAccount': 'Můj účet', - 'settings.integrationsTitle': 'Marketplace a faktury', + 'settings.integrationsTitle': 'Platby, dopravci a marketplace', 'settings.integrationsHint': - 'Nastavení Allegro, Amazon a iFirma. Sync objednávek a auto-faktury použijí tato nastavení po zapnutí konektorů.', + 'PayU, InPost, DHL, DPD, Allegro, Amazon a iFirma — uloženo v databázi. Prázdné tajné pole ponechá uloženou hodnotu.', 'settings.secretSaved': 'uloženo', + 'settings.shippingSandbox': 'Použít sandbox / testovací API dopravy', + 'settings.live': 'Produkce', + 'settings.payuPosId': 'PayU POS ID', + 'settings.payuClientId': 'PayU client ID', + 'settings.payuClientSecret': 'PayU client secret', + 'settings.payuSecondKey': 'PayU second key (MD5)', + 'settings.payuOauthUrl': 'PayU OAuth URL', + 'settings.payuOrdersUrl': 'PayU orders URL', + 'settings.payuNotifyUrl': 'PayU notify (webhook) URL', + 'settings.payuContinueUrl': 'PayU continue URL (základ)', + 'settings.inPostOrgId': 'InPost organization ID', + 'settings.inPostShipXToken': 'InPost ShipX token', + 'settings.inPostShipXBaseUrl': 'InPost ShipX base URL', + 'settings.inPostGeowidgetConfig': 'Geowidget config', + 'settings.inPostGeowidgetSdk': 'Geowidget SDK base URL', + 'settings.inPostGeowidgetPublic': 'Geowidget public token', + 'settings.inPostGeowidgetSandbox': 'Geowidget sandbox token', + 'settings.dhlApiKey': 'DHL API key', + 'settings.dhlApiSecret': 'DHL API secret', + 'settings.dhlApiBaseUrl': 'DHL API base URL', + 'settings.dhlWidgetUrl': 'DHL mapa Parcelshop', + 'settings.dpdLogin': 'DPD login', + 'settings.dpdPassword': 'DPD heslo', + 'settings.dpdFid': 'DPD FID', + 'settings.dpdApiBaseUrl': 'DPD API base URL', + 'settings.dpdWidgetUrl': 'DPD pudofinder URL', 'settings.allegroClientId': 'Allegro client ID', 'settings.allegroClientSecret': 'Allegro client secret', 'settings.allegroSellerId': 'Allegro seller ID', @@ -514,7 +575,7 @@ export const CS_TRANSLATIONS: TranslationMap = { 'settings.mode': 'Režim', 'settings.sandbox': 'Sandbox', 'settings.shippingHint': - 'Přihlašovací údaje zůstávají v konfiguraci serveru — zde jen flagy viditelnosti.', + 'Dopravce zapněte zde. API tokeny a URL widgetů nastavíte v Integracích.', 'settings.emailTitle': 'E-mail / newsletter', 'settings.fromName': 'Jméno odesílatele', 'settings.fromAddress': 'Adresa odesílatele', diff --git a/src/admin/src/app/core/i18n/translations/de.ts b/src/admin/src/app/core/i18n/translations/de.ts index cd8fba1..979df28 100644 --- a/src/admin/src/app/core/i18n/translations/de.ts +++ b/src/admin/src/app/core/i18n/translations/de.ts @@ -53,14 +53,40 @@ export const DE_TRANSLATIONS: TranslationMap = { 'dashboard.errorLoad': 'Dashboard konnte nicht geladen werden', 'dashboard.alertLowStock': 'SKU mit niedrigem Bestand', 'dashboard.alertPending': 'warten auf Zahlung', + 'dashboard.alertReviews': 'Bewertungen zur Moderation', + 'dashboard.viewOrders': 'Bestellungen anzeigen', + 'dashboard.openPickList': 'Kommissionierung öffnen', + 'dashboard.pendingFulfillment': 'Offene Erfüllung', + 'dashboard.pendingFulfillmentHint': 'Braucht Aufmerksamkeit', + 'dashboard.lowStockHint': 'Schwelle ≤ 5', + 'dashboard.needsAction': 'Handlungsbedarf — Bestellungen', + 'dashboard.pendingReviews': 'Offene Bewertungen', + 'dashboard.moderate': 'Moderieren', + 'dashboard.restock': 'Auffüllen', + 'dashboard.noActionOrders': 'Keine Bestellungen brauchen gerade Aktion.', + 'dashboard.noLowStock': 'Bestände sehen gesund aus.', + 'dashboard.noPendingReviews': 'Keine Bewertungen warten.', + 'orders.purpose': + 'Erfüllen und klären — mehrere auswählen zum Kommissionieren, Rechnungen oder Labels.', + 'orders.order': 'Bestellung', 'orders.bulkSelected': '{count} ausgewählt', 'orders.bulkPick': 'Kommissionieren', 'orders.bulkInvoices': 'Rechnungen erzeugen', 'orders.bulkLabels': 'Labels erzeugen', 'orders.bulkLabelsSoon': 'Massen-Labels erzeugen Versandetiketten für die Auswahl.', + 'orders.bulkLabelsEmpty': 'Zuerst Bestellungen auswählen', + 'orders.bulkLabelsDone': + 'Labels: {{ok}} erstellt, {{fail}} fehlgeschlagen, {{skip}} übersprungen', + 'orders.bulkLabelsError': 'Massen-Labels fehlgeschlagen', 'orders.selectAll': 'Alle auswählen', 'orders.selectRow': 'Bestellung auswählen', 'products.title': 'Produkte', + 'products.purpose': 'Verkaufskatalog und Lager verwalten.', + 'promotions.purpose': 'Kampagnen und Codes ohne Verwirrung führen.', + 'newsletter.purpose': + 'Mit Abonnenten kommunizieren — verfassen, Vorschau, sicher senden.', + 'reviews.purpose': + 'Moderationswarteschlange — Vertrauen im Shop schützen.', 'products.add': 'Produkt hinzufügen', 'products.edit': 'Bearbeiten', 'products.editTitle': 'Produkt bearbeiten', @@ -237,6 +263,11 @@ export const DE_TRANSLATIONS: TranslationMap = { 'orders.markCollected': 'Kommissioniert', 'orders.collected': 'Kommissioniert', 'orders.showCollected': 'Kommissionierte anzeigen', + 'orders.pickByOrder': 'Nach Bestellung', + 'orders.pickByProduct': 'Nach Produkt', + 'orders.pickTotalQty': 'Gesamt zu holen: {{count}}', + 'orders.collectProduct': 'Alle Positionen holen', + 'orders.productCollected': 'Alle Positionen geholt', 'orders.pendingItems': 'Offen', 'orders.collectedItems': 'Kommissioniert', 'orders.orderWarehouse': 'Lager für Bestellung', @@ -294,6 +325,7 @@ export const DE_TRANSLATIONS: TranslationMap = { 'promotions.kpiActive': 'Aktive Aktionen', 'promotions.kpiRedemptions': 'Einlösungen (Seite)', 'promotions.kpiCodes': 'Codes (Seite)', + 'promotions.kpiDiscount': 'Gewährte Rabatte (30 Tage)', 'promotions.sectionBenefit': 'Vorteil', 'promotions.sectionRequirements': 'Voraussetzungen', 'promotions.sectionLimits': 'Nutzungslimits', @@ -330,6 +362,7 @@ export const DE_TRANSLATIONS: TranslationMap = { 'newsletter.searchCampaign': 'Kampagnen suchen…', 'newsletter.joined': 'Beigetreten', 'newsletter.sent': 'Gesendet', + 'newsletter.recipients': 'Empfänger', 'newsletter.unsubscribed': 'Abgemeldet', 'newsletter.status.sent': 'Gesendet', 'newsletter.status.draft': 'Entwurf', @@ -429,6 +462,8 @@ export const DE_TRANSLATIONS: TranslationMap = { 'settings.title': 'Einstellungen', 'settings.purpose': 'Shop-Konfiguration — lokal gespeichert bis Settings-APIs existieren.', + 'settings.errorLoad': 'Einstellungen konnten nicht geladen werden', + 'settings.errorSave': 'Einstellungen konnten nicht gespeichert werden', 'settings.save': 'Änderungen speichern', 'settings.navStore': 'Shop-Profil', 'settings.navLocales': 'Sprachen & Währung', @@ -437,10 +472,36 @@ export const DE_TRANSLATIONS: TranslationMap = { 'settings.navIntegrations': 'Integrationen', 'settings.navEmail': 'E-Mail', 'settings.navAccount': 'Mein Konto', - 'settings.integrationsTitle': 'Marktplätze & Rechnungen', + 'settings.integrationsTitle': 'Zahlungen, Carrier & Marktplätze', 'settings.integrationsHint': - 'Allegro-, Amazon- und iFirma-Zugangsdaten. Sync und Auto-Rechnung nutzen diese Einstellungen, sobald die Connectoren aktiv sind.', + 'PayU, InPost, DHL, DPD, Allegro, Amazon und iFirma — in der Datenbank. Leeres Geheimnisfeld behält den gespeicherten Wert.', 'settings.secretSaved': 'gespeichert', + 'settings.shippingSandbox': 'Shipping-Sandbox / Test-APIs nutzen', + 'settings.live': 'Live', + 'settings.payuPosId': 'PayU POS-ID', + 'settings.payuClientId': 'PayU Client-ID', + 'settings.payuClientSecret': 'PayU Client-Secret', + 'settings.payuSecondKey': 'PayU Second Key (MD5)', + 'settings.payuOauthUrl': 'PayU OAuth-URL', + 'settings.payuOrdersUrl': 'PayU Orders-URL', + 'settings.payuNotifyUrl': 'PayU Notify-(Webhook)-URL', + 'settings.payuContinueUrl': 'PayU Continue-URL (Basis)', + 'settings.inPostOrgId': 'InPost Organisations-ID', + 'settings.inPostShipXToken': 'InPost ShipX-Token', + 'settings.inPostShipXBaseUrl': 'InPost ShipX Base-URL', + 'settings.inPostGeowidgetConfig': 'Geowidget-Config', + 'settings.inPostGeowidgetSdk': 'Geowidget SDK Base-URL', + 'settings.inPostGeowidgetPublic': 'Geowidget Public-Token', + 'settings.inPostGeowidgetSandbox': 'Geowidget Sandbox-Token', + 'settings.dhlApiKey': 'DHL API-Key', + 'settings.dhlApiSecret': 'DHL API-Secret', + 'settings.dhlApiBaseUrl': 'DHL API Base-URL', + 'settings.dhlWidgetUrl': 'DHL Parcelshop-Karten-URL', + 'settings.dpdLogin': 'DPD Login', + 'settings.dpdPassword': 'DPD Passwort', + 'settings.dpdFid': 'DPD FID', + 'settings.dpdApiBaseUrl': 'DPD API Base-URL', + 'settings.dpdWidgetUrl': 'DPD Pudofinder-URL', 'settings.allegroClientId': 'Allegro Client-ID', 'settings.allegroClientSecret': 'Allegro Client-Secret', 'settings.allegroSellerId': 'Allegro Verkäufer-ID', @@ -514,7 +575,7 @@ export const DE_TRANSLATIONS: TranslationMap = { 'settings.mode': 'Modus', 'settings.sandbox': 'Sandbox', 'settings.shippingHint': - 'Zugangsdaten bleiben in der Server-Konfiguration — hier nur Sichtbarkeitsflags.', + 'Carrier hier aktivieren. API-Tokens und Widget-URLs unter Integrationen.', 'settings.emailTitle': 'E-Mail / Newsletter', 'settings.fromName': 'Absendername', 'settings.fromAddress': 'Absenderadresse', diff --git a/src/admin/src/app/core/i18n/translations/en.ts b/src/admin/src/app/core/i18n/translations/en.ts index f92fd99..9de23d7 100644 --- a/src/admin/src/app/core/i18n/translations/en.ts +++ b/src/admin/src/app/core/i18n/translations/en.ts @@ -261,6 +261,11 @@ export const EN_TRANSLATIONS: TranslationMap = { 'orders.markCollected': 'Mark collected', 'orders.collected': 'Collected', 'orders.showCollected': 'Show collected', + 'orders.pickByOrder': 'By order', + 'orders.pickByProduct': 'By product', + 'orders.pickTotalQty': 'Total to pick: {{count}}', + 'orders.collectProduct': 'Collect all lines', + 'orders.productCollected': 'All lines collected', 'orders.pendingItems': 'Pending', 'orders.collectedItems': 'Collected', 'orders.orderWarehouse': 'Warehouse for order', @@ -463,10 +468,36 @@ export const EN_TRANSLATIONS: TranslationMap = { 'settings.navIntegrations': 'Integrations', 'settings.navEmail': 'Email', 'settings.navAccount': 'My account', - 'settings.integrationsTitle': 'Marketplace & invoicing', + 'settings.integrationsTitle': 'Payments, carriers & marketplaces', 'settings.integrationsHint': - 'Configure Allegro, Amazon, and iFirma credentials. Order sync and auto-invoicing use these settings once connectors are live.', + 'PayU, InPost, DHL, DPD, Allegro, Amazon, and iFirma — stored in the database. Leave a secret blank to keep the saved value.', 'settings.secretSaved': 'saved', + 'settings.shippingSandbox': 'Use shipping sandbox / test APIs', + 'settings.live': 'Live', + 'settings.payuPosId': 'PayU POS ID', + 'settings.payuClientId': 'PayU client ID', + 'settings.payuClientSecret': 'PayU client secret', + 'settings.payuSecondKey': 'PayU second key (MD5)', + 'settings.payuOauthUrl': 'PayU OAuth URL', + 'settings.payuOrdersUrl': 'PayU orders URL', + 'settings.payuNotifyUrl': 'PayU notify (webhook) URL', + 'settings.payuContinueUrl': 'PayU continue URL base', + 'settings.inPostOrgId': 'InPost organization ID', + 'settings.inPostShipXToken': 'InPost ShipX token', + 'settings.inPostShipXBaseUrl': 'InPost ShipX base URL', + 'settings.inPostGeowidgetConfig': 'Geowidget config', + 'settings.inPostGeowidgetSdk': 'Geowidget SDK base URL', + 'settings.inPostGeowidgetPublic': 'Geowidget public token', + 'settings.inPostGeowidgetSandbox': 'Geowidget sandbox token', + 'settings.dhlApiKey': 'DHL API key', + 'settings.dhlApiSecret': 'DHL API secret', + 'settings.dhlApiBaseUrl': 'DHL API base URL', + 'settings.dhlWidgetUrl': 'DHL parcel shop map URL', + 'settings.dpdLogin': 'DPD login', + 'settings.dpdPassword': 'DPD password', + 'settings.dpdFid': 'DPD FID', + 'settings.dpdApiBaseUrl': 'DPD API base URL', + 'settings.dpdWidgetUrl': 'DPD pudofinder URL', 'settings.allegroClientId': 'Allegro client ID', 'settings.allegroClientSecret': 'Allegro client secret', 'settings.allegroSellerId': 'Allegro seller ID', @@ -540,7 +571,7 @@ export const EN_TRANSLATIONS: TranslationMap = { 'settings.mode': 'Mode', 'settings.sandbox': 'Sandbox', 'settings.shippingHint': - 'Credentials stay in server config — this UI only stores visibility flags locally.', + 'Enable carriers here. API tokens and widget URLs are configured under Integrations.', 'settings.emailTitle': 'Email / newsletter', 'settings.fromName': 'From name', 'settings.fromAddress': 'From address', diff --git a/src/admin/src/app/core/i18n/translations/pl.ts b/src/admin/src/app/core/i18n/translations/pl.ts index 1f7a977..e972979 100644 --- a/src/admin/src/app/core/i18n/translations/pl.ts +++ b/src/admin/src/app/core/i18n/translations/pl.ts @@ -53,6 +53,20 @@ export const PL_TRANSLATIONS: TranslationMap = { 'dashboard.errorLoad': 'Nie udało się wczytać pulpitu', 'dashboard.alertLowStock': 'SKU z niskim stanem', 'dashboard.alertPending': 'oczekuje na płatność', + 'dashboard.alertReviews': 'opinii do moderacji', + 'dashboard.viewOrders': 'Zobacz zamówienia', + 'dashboard.openPickList': 'Otwórz listę kompletacji', + 'dashboard.pendingFulfillment': 'Do realizacji', + 'dashboard.pendingFulfillmentHint': 'Wymaga uwagi', + 'dashboard.lowStockHint': 'Próg ≤ 5', + 'dashboard.needsAction': 'Wymaga działania — zamówienia', + 'dashboard.pendingReviews': 'Opinie do moderacji', + 'dashboard.moderate': 'Moderuj', + 'dashboard.restock': 'Uzupełnij', + 'dashboard.noActionOrders': 'Brak zamówień wymagających działania.', + 'dashboard.noLowStock': 'Stany magazynowe wyglądają dobrze.', + 'dashboard.noPendingReviews': 'Brak opinii oczekujących.', + 'orders.order': 'Zamówienie', 'orders.bulkSelected': 'Wybrano {count}', 'orders.bulkPick': 'Kompletuj / pick', 'orders.bulkInvoices': 'Generuj faktury', @@ -247,6 +261,11 @@ export const PL_TRANSLATIONS: TranslationMap = { 'orders.markCollected': 'Skompletowano', 'orders.collected': 'Skompletowane', 'orders.showCollected': 'Pokaż skompletowane', + 'orders.pickByOrder': 'Wg zamówień', + 'orders.pickByProduct': 'Wg produktów', + 'orders.pickTotalQty': 'Razem do pobrania: {{count}}', + 'orders.collectProduct': 'Skompletuj wszystkie', + 'orders.productCollected': 'Wszystkie skompletowane', 'orders.pendingItems': 'Do kompletacji', 'orders.collectedItems': 'Skompletowane', 'orders.orderWarehouse': 'Magazyn dla zamówienia', @@ -341,6 +360,7 @@ export const PL_TRANSLATIONS: TranslationMap = { 'newsletter.searchCampaign': 'Szukaj kampanii…', 'newsletter.joined': 'Dołączył', 'newsletter.sent': 'Wysłano', + 'newsletter.recipients': 'Odbiorcy', 'newsletter.unsubscribed': 'Wypisany', 'newsletter.status.sent': 'Wysłana', 'newsletter.status.draft': 'Szkic', @@ -451,10 +471,36 @@ export const PL_TRANSLATIONS: TranslationMap = { 'settings.navIntegrations': 'Integracje', 'settings.navEmail': 'Email', 'settings.navAccount': 'Moje konto', - 'settings.integrationsTitle': 'Marketplace i faktury', + 'settings.integrationsTitle': 'Płatności, przewoźnicy i marketplace', 'settings.integrationsHint': - 'Konfiguracja Allegro, Amazon i iFirma. Synchronizacja zamówień i auto-faktury użyją tych ustawień po włączeniu konektorów.', + 'PayU, InPost, DHL, DPD, Allegro, Amazon i iFirma — zapis w bazie. Puste tajne pole zostawia dotychczasową wartość.', 'settings.secretSaved': 'zapisany', + 'settings.shippingSandbox': 'Używaj sandbox / testowych API wysyłki', + 'settings.live': 'Produkcja', + 'settings.payuPosId': 'PayU POS ID', + 'settings.payuClientId': 'PayU client ID', + 'settings.payuClientSecret': 'PayU client secret', + 'settings.payuSecondKey': 'PayU second key (MD5)', + 'settings.payuOauthUrl': 'PayU OAuth URL', + 'settings.payuOrdersUrl': 'PayU orders URL', + 'settings.payuNotifyUrl': 'PayU notify (webhook) URL', + 'settings.payuContinueUrl': 'PayU continue URL (baza)', + 'settings.inPostOrgId': 'InPost organization ID', + 'settings.inPostShipXToken': 'InPost ShipX token', + 'settings.inPostShipXBaseUrl': 'InPost ShipX base URL', + 'settings.inPostGeowidgetConfig': 'Geowidget config', + 'settings.inPostGeowidgetSdk': 'Geowidget SDK base URL', + 'settings.inPostGeowidgetPublic': 'Geowidget public token', + 'settings.inPostGeowidgetSandbox': 'Geowidget sandbox token', + 'settings.dhlApiKey': 'DHL API key', + 'settings.dhlApiSecret': 'DHL API secret', + 'settings.dhlApiBaseUrl': 'DHL API base URL', + 'settings.dhlWidgetUrl': 'DHL mapa Parcelshop', + 'settings.dpdLogin': 'DPD login', + 'settings.dpdPassword': 'DPD hasło', + 'settings.dpdFid': 'DPD FID', + 'settings.dpdApiBaseUrl': 'DPD API base URL', + 'settings.dpdWidgetUrl': 'DPD pudofinder URL', 'settings.allegroClientId': 'Allegro client ID', 'settings.allegroClientSecret': 'Allegro client secret', 'settings.allegroSellerId': 'Allegro seller ID', @@ -528,7 +574,7 @@ export const PL_TRANSLATIONS: TranslationMap = { 'settings.mode': 'Tryb', 'settings.sandbox': 'Sandbox', 'settings.shippingHint': - 'Dane dostępowe zostają w konfiguracji serwera — tu tylko flagi widoczności.', + 'Włącz przewoźników tutaj. Tokeny API i URL widgetów ustawisz w Integracjach.', 'settings.emailTitle': 'Email / newsletter', 'settings.fromName': 'Nazwa nadawcy', 'settings.fromAddress': 'Adres nadawcy', diff --git a/src/admin/src/app/features/categories/categories-admin.component.html b/src/admin/src/app/features/categories/categories-admin.component.html index 63872ed..745f98a 100644 --- a/src/admin/src/app/features/categories/categories-admin.component.html +++ b/src/admin/src/app/features/categories/categories-admin.component.html @@ -222,7 +222,11 @@ > {{ 'products.save' | translate }} - + {{ 'categories.viewProducts' | translate }} diff --git a/src/admin/src/app/features/orders/order-channel-icons.component.ts b/src/admin/src/app/features/orders/order-channel-icons.component.ts new file mode 100644 index 0000000..e0e854a --- /dev/null +++ b/src/admin/src/app/features/orders/order-channel-icons.component.ts @@ -0,0 +1,112 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { + LucideMapPinned, + LucidePackage, + LucidePenLine, + LucideStore, + LucideTag, + LucideTruck, +} from '@lucide/angular'; +import { TranslatePipe } from '../../core/i18n/translate.pipe'; +import { + OrderSource, + ShippingCarrier, + ShippingServiceType, +} from './order.models'; + +/** + * Compact channel + delivery icons for order tables and headers. + */ +@Component({ + selector: 'app-order-channel-icons', + standalone: true, + imports: [ + TranslatePipe, + LucideStore, + LucideTag, + LucidePackage, + LucidePenLine, + LucideTruck, + LucideMapPinned, + ], + template: ` +
+ + @switch (channel()) { + @case ('allegro') { + + } + @case ('amazon') { + + } + @case ('manual') { + + } + @default { + + } + } + @if (showLabels()) { + + {{ 'orders.source.' + channel() | translate }} + + } + + + + @if (service() === 'courier') { + + } @else { + + } + @if (showLabels()) { + + {{ 'orders.carrier.' + carrier() | translate }} + · + {{ 'orders.service.' + service() | translate }} + + } + +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrderChannelIconsComponent { + readonly source = input('shop'); + readonly shippingCarrier = input< + ShippingCarrier | string | null | undefined + >('inPost'); + readonly shippingServiceType = input< + ShippingServiceType | string | null | undefined + >('parcelLocker'); + readonly showLabels = input(false); + readonly iconSize = input(14); + + protected channel(): OrderSource { + const value = this.source() || 'shop'; + return value as OrderSource; + } + + protected carrier(): ShippingCarrier { + const value = this.shippingCarrier() || 'inPost'; + return value as ShippingCarrier; + } + + protected service(): ShippingServiceType { + const value = this.shippingServiceType() || 'parcelLocker'; + return value as ShippingServiceType; + } +} diff --git a/src/admin/src/app/features/orders/order-detail-modal.component.html b/src/admin/src/app/features/orders/order-detail-modal.component.html index 4e76e05..1b654f8 100644 --- a/src/admin/src/app/features/orders/order-detail-modal.component.html +++ b/src/admin/src/app/features/orders/order-detail-modal.component.html @@ -18,22 +18,14 @@ } @if (detail(); as d) { -
- - {{ 'orders.source.' + (d.source || 'shop') | translate }} - - - {{ 'orders.carrier.' + d.shippingCarrier | translate }} - · - {{ 'orders.service.' + d.shippingServiceType | translate }} - +
+ @if (d.externalOrderId) { {{ d.externalOrderId }} diff --git a/src/admin/src/app/features/orders/order-detail-modal.component.ts b/src/admin/src/app/features/orders/order-detail-modal.component.ts index 7fc0617..9d0997a 100644 --- a/src/admin/src/app/features/orders/order-detail-modal.component.ts +++ b/src/admin/src/app/features/orders/order-detail-modal.component.ts @@ -15,6 +15,7 @@ import { TranslatePipe } from '../../core/i18n/translate.pipe'; import { ApiService } from '../../core/api.service'; import { API_BASE_URL } from '../../core/constants'; import { AdminSkeletonComponent } from '../../shared/admin-skeleton.component'; +import { OrderChannelIconsComponent } from './order-channel-icons.component'; import { AdminOrderDetail, AdminOrderDetailLine, @@ -40,6 +41,7 @@ import { TranslatePipe, I18nCurrencyPipe, AdminSkeletonComponent, + OrderChannelIconsComponent, ], templateUrl: './order-detail-modal.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/admin/src/app/features/orders/order-pick.utils.ts b/src/admin/src/app/features/orders/order-pick.utils.ts index 861f6cc..ccb2b57 100644 --- a/src/admin/src/app/features/orders/order-pick.utils.ts +++ b/src/admin/src/app/features/orders/order-pick.utils.ts @@ -1,4 +1,9 @@ -import { PickListLine, PickListOrderGroup, PickListWarehouseQty } from './order.models'; +import { + PickListLine, + PickListOrderGroup, + PickListProductGroup, + PickListWarehouseQty, +} from './order.models'; export interface CollectLinePayload { orderLineId: string; @@ -36,6 +41,66 @@ export function groupPickListByOrder( ); } +/** + * Aggregates pick lines by product variant so pickers can + * pull total units once (e.g. Gold 1 + Gold 2 → Gold 3). + */ +export function groupPickListByProduct( + items: PickListLine[], +): PickListProductGroup[] { + const groups = new Map(); + + 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 }} + + } +
+ + + + + + + + + + + + + @for (line of group.lines; track line.orderLineId) { + + + + + + + + } + +
{{ 'orders.order' | translate }}{{ 'orders.qty' | translate }}{{ 'orders.warehouseStock' | translate }}{{ 'orders.pickFrom' | translate }}{{ 'orders.status' | translate }}
+ #{{ 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 @@ /> @@ -272,8 +290,17 @@ /> @@ -288,15 +315,24 @@ />
- - {{ 'settings.sandbox' | translate }} + + {{ + (form.shippingUseSandbox + ? 'settings.sandbox' + : 'settings.live' + ) | translate + }}
- - {{ 'settings.sandbox' | translate }} + + {{ + (form.shippingUseSandbox + ? 'settings.sandbox' + : 'settings.live' + ) | translate + }}
- - {{ 'settings.sandbox' | translate }} + + {{ + (form.shippingUseSandbox + ? 'settings.sandbox' + : 'settings.live' + ) | translate + }}
-

+

{{ 'settings.shippingHint' | translate }}

@@ -313,6 +349,335 @@ {{ 'settings.integrationsHint' | translate }}

+
+
+ PayU + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ InPost +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ DHL +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ DPD +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
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("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsApproved") + .HasColumnType("boolean"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("PublicReply") + .HasColumnType("text"); + + b.Property("Rating") + .HasColumnType("integer"); + + b.Property("RepliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProductId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LowStockThreshold") + .HasColumnType("integer"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProductVariantId") + .IsUnique(); + + b.ToTable("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WarehouseId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ProductVariantId", "WarehouseId") + .IsUnique(); + + b.ToTable("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", 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("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssuedForOrderId") + .HasColumnType("uuid"); + + b.Property("IssuedToEmail") + .HasColumnType("text"); + + b.Property("MaxUsages") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UsageCount") + .HasColumnType("integer"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("DiscountRuleId"); + + b.ToTable("DiscountCodes"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("DiscountRuleId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("DiscountRuleId", "CustomerEmail"); + + b.ToTable("DiscountRedemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerHighlightedCode") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IssueRewardOnPaidOrder") + .HasColumnType("boolean"); + + b.Property("MaxRedemptionsPerCustomer") + .HasColumnType("integer"); + + b.Property("MaxTotalRedemptions") + .HasColumnType("integer"); + + b.Property("MinimumOrderAmount") + .HasColumnType("numeric"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RewardCodePrefix") + .HasColumnType("text"); + + b.Property("RewardMinPaidOrderTotal") + .HasColumnType("numeric"); + + b.Property("RewardValidDays") + .HasColumnType("integer"); + + b.Property("ShowInBanner") + .HasColumnType("boolean"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("DiscountRules"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHtml") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("NewsletterCampaigns"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UnsubscribedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("NewsletterSubscribers"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("HighlightedCode") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PromoBanners"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BillingCity") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("BillingPostalCode") + .HasColumnType("text"); + + b.Property("BillingStreet") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerFirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerLastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerNotes") + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeliveryCost") + .HasColumnType("numeric"); + + b.Property("DiscountAmount") + .HasColumnType("numeric"); + + b.Property("DiscountCodeId") + .HasColumnType("uuid"); + + b.Property("ExternalOrderId") + .HasColumnType("text"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("PaymentMethod") + .HasColumnType("integer"); + + b.Property("ShippingCarrier") + .HasColumnType("integer"); + + b.Property("ShippingCity") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingCountry") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingPostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("ShippingServiceType") + .HasColumnType("integer"); + + b.Property("ShippingStreet") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Subtotal") + .HasColumnType("numeric"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("VatAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("DiscountCodeId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.HasIndex("Source", "ExternalOrderId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CollectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("PickedFromWarehouseId") + .HasColumnType("uuid"); + + b.Property("ProductId") + .HasColumnType("uuid"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProductVariantId") + .HasColumnType("uuid"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnitPrice") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VariantColor") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("PickedFromWarehouseId"); + + b.ToTable("OrderLines"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("text"); + + b.Property("RawResponse") + .HasColumnType("text"); + + b.Property("RedirectUrl") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Carrier") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExternalShipmentId") + .HasColumnType("text"); + + b.Property("LabelRelativePath") + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("ParcelPointAddress") + .HasColumnType("text"); + + b.Property("ParcelPointId") + .HasColumnType("text"); + + b.Property("ParcelPointName") + .HasColumnType("text"); + + b.Property("ServiceType") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TrackingNumber") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrderId") + .IsUnique(); + + b.ToTable("Shipments"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Cart.CartItem", b => + { + b.HasOne("iLED.Domain.Cart.Cart", "Cart") + .WithMany("Items") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.FavoriteItem", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "ParentCategory") + .WithMany("SubCategories") + .HasForeignKey("ParentCategoryId"); + + b.Navigation("ParentCategory"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.HasOne("iLED.Domain.Catalog.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.ProductGroup", "ProductGroup") + .WithMany("Products") + .HasForeignKey("ProductGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Descriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Descriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "ShortDescriptions", b1 => + { + b1.Property("ProductId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductId"); + + b1.ToTable("Products"); + + b1 + .ToJson("ShortDescriptions") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductId"); + }); + + b.Navigation("Category"); + + b.Navigation("Descriptions") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("ProductGroup"); + + b.Navigation("ShortDescriptions") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductAttribute", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Attributes") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Values", b1 => + { + b1.Property("ProductAttributeId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductAttributeId"); + + b1.ToTable("ProductAttributes"); + + b1 + .ToJson("Values") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductAttributeId"); + }); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Values") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductChannelPrice", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("ChannelPrices") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Names", b1 => + { + b1.Property("ProductGroupId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("ProductGroupId"); + + b1.ToTable("ProductGroups"); + + b1 + .ToJson("Names") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("ProductGroupId"); + }); + + b.Navigation("Names") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductImage", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Images") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Variants") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Review", b => + { + b.HasOne("iLED.Domain.Catalog.Product", "Product") + .WithMany("Reviews") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Stock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithOne("Stock") + .HasForeignKey("iLED.Domain.Catalog.Stock", "ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.WarehouseStock", b => + { + b.HasOne("iLED.Domain.Catalog.ProductVariant", "ProductVariant") + .WithMany("WarehouseStocks") + .HasForeignKey("ProductVariantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "Warehouse") + .WithMany("Stocks") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProductVariant"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.RefreshToken", b => + { + b.HasOne("iLED.Domain.Identity.ApplicationUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.CampaignSend", b => + { + b.HasOne("iLED.Domain.Marketing.NewsletterCampaign", "Campaign") + .WithMany("Sends") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.NewsletterSubscriber", "Subscriber") + .WithMany("CampaignSends") + .HasForeignKey("SubscriberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Campaign"); + + b.Navigation("Subscriber"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountCode", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Codes") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRedemption", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("iLED.Domain.Marketing.DiscountRule", "DiscountRule") + .WithMany("Redemptions") + .HasForeignKey("DiscountRuleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DiscountCode"); + + b.Navigation("DiscountRule"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "BannerTexts", b1 => + { + b1.Property("DiscountRuleId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("DiscountRuleId"); + + b1.ToTable("DiscountRules"); + + b1 + .ToJson("BannerTexts") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("DiscountRuleId"); + }); + + b.Navigation("BannerTexts") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.PromoBanner", b => + { + b.OwnsOne("iLED.Domain.Common.LocalizedStrings", "Texts", b1 => + { + b1.Property("PromoBannerId"); + + b1.Property("Cs") + .IsRequired(); + + b1.Property("De") + .IsRequired(); + + b1.Property("En") + .IsRequired(); + + b1.Property("Pl") + .IsRequired(); + + b1.HasKey("PromoBannerId"); + + b1.ToTable("PromoBanners"); + + b1 + .ToJson("Texts") + .HasColumnType("jsonb"); + + b1.WithOwner() + .HasForeignKey("PromoBannerId"); + }); + + b.Navigation("Texts") + .IsRequired(); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.HasOne("iLED.Domain.Marketing.DiscountCode", "DiscountCode") + .WithMany() + .HasForeignKey("DiscountCodeId"); + + b.Navigation("DiscountCode"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.OrderLine", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithMany("Lines") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("iLED.Domain.Catalog.Warehouse", "PickedFromWarehouse") + .WithMany() + .HasForeignKey("PickedFromWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Order"); + + b.Navigation("PickedFromWarehouse"); + }); + + modelBuilder.Entity("iLED.Domain.Payments.Payment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Payment") + .HasForeignKey("iLED.Domain.Payments.Payment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Shipping.Shipment", b => + { + b.HasOne("iLED.Domain.Orders.Order", "Order") + .WithOne("Shipment") + .HasForeignKey("iLED.Domain.Shipping.Shipment", "OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("iLED.Domain.Cart.Cart", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Category", b => + { + b.Navigation("Products"); + + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Product", b => + { + b.Navigation("Attributes"); + + b.Navigation("ChannelPrices"); + + b.Navigation("Images"); + + b.Navigation("Reviews"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductGroup", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.ProductVariant", b => + { + b.Navigation("Stock") + .IsRequired(); + + b.Navigation("WarehouseStocks"); + }); + + modelBuilder.Entity("iLED.Domain.Catalog.Warehouse", b => + { + b.Navigation("Stocks"); + }); + + modelBuilder.Entity("iLED.Domain.Identity.ApplicationUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.DiscountRule", b => + { + b.Navigation("Codes"); + + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterCampaign", b => + { + b.Navigation("Sends"); + }); + + modelBuilder.Entity("iLED.Domain.Marketing.NewsletterSubscriber", b => + { + b.Navigation("CampaignSends"); + }); + + modelBuilder.Entity("iLED.Domain.Orders.Order", b => + { + b.Navigation("Lines"); + + b.Navigation("Payment"); + + b.Navigation("Shipment"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261019130010_ShippingPayUIntegrations.cs b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261019130010_ShippingPayUIntegrations.cs new file mode 100644 index 0000000..b6e9744 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Persistence/Migrations/20261019130010_ShippingPayUIntegrations.cs @@ -0,0 +1,280 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace iLED.Infrastructure.Persistence.Migrations +{ + /// + public partial class ShippingPayUIntegrations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DhlApiBaseUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DhlApiKey", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DhlApiSecret", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DhlWidgetUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DpdApiBaseUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DpdFid", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DpdLogin", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DpdPassword", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "DpdWidgetUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostGeowidgetConfig", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostGeowidgetPublicToken", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostGeowidgetSandboxToken", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostGeowidgetSdkBaseUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostOrganizationId", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostShipXBaseUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "InPostShipXToken", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUClientId", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUClientSecret", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUContinueUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUNotifyUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUOauthUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUOrdersUrl", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUPosId", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUSecondKey", + table: "StoreSettings", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayUUseSandbox", + table: "StoreSettings", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ShippingUseSandbox", + table: "StoreSettings", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DhlApiBaseUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DhlApiKey", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DhlApiSecret", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DhlWidgetUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DpdApiBaseUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DpdFid", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DpdLogin", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DpdPassword", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "DpdWidgetUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostGeowidgetConfig", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostGeowidgetPublicToken", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostGeowidgetSandboxToken", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostGeowidgetSdkBaseUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostOrganizationId", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostShipXBaseUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "InPostShipXToken", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUClientId", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUClientSecret", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUContinueUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUNotifyUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUOauthUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUOrdersUrl", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUPosId", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUSecondKey", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "PayUUseSandbox", + table: "StoreSettings"); + + migrationBuilder.DropColumn( + name: "ShippingUseSandbox", + table: "StoreSettings"); + } + } +} diff --git a/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs b/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs index bfa92b6..8da56c1 100644 --- a/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs +++ b/src/backend/src/iLED.Infrastructure/Services/DpdPudoClient.cs @@ -4,7 +4,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using iLED.Application.Common.Interfaces; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace iLED.Infrastructure.Services; @@ -15,7 +14,7 @@ namespace iLED.Infrastructure.Services; /// public sealed class DpdPudoClient( IHttpClientFactory httpClientFactory, - IConfiguration configuration, + IIntegrationSettingsAccessor integrationSettings, ILogger logger) { private const string DefaultBaseUrl = "https://pudofinder.dpd.com.pl"; @@ -90,7 +89,7 @@ public sealed class DpdPudoClient( private async Task> GetCatalogueAsync( CancellationToken cancellationToken) { - var baseUrl = GetBaseUrl(); + var baseUrl = await GetBaseUrlAsync(cancellationToken); if (Cache.TryGetValue(baseUrl, out var cached) && cached.ExpiresAt > DateTimeOffset.UtcNow) { @@ -130,7 +129,7 @@ public sealed class DpdPudoClient( } var client = httpClientFactory.CreateClient(nameof(DpdPudoClient)); - var baseUrl = GetBaseUrl(); + var baseUrl = await GetBaseUrlAsync(cancellationToken); var url = $"{baseUrl}/ajax/points/details?lang=pl"; using var content = new FormUrlEncodedContent( @@ -161,9 +160,10 @@ public sealed class DpdPudoClient( StringComparer.OrdinalIgnoreCase); } - private string GetBaseUrl() + private async Task GetBaseUrlAsync(CancellationToken cancellationToken) { - var configured = configuration["Shipping:Dpd:WidgetUrl"]; + var settings = await integrationSettings.GetAsync(cancellationToken); + var configured = settings.DpdWidgetUrl; if (string.IsNullOrWhiteSpace(configured)) { return DefaultBaseUrl; diff --git a/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs b/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs index 9796e25..1266e7c 100644 --- a/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs +++ b/src/backend/src/iLED.Infrastructure/Services/ExternalServices.cs @@ -11,19 +11,20 @@ namespace iLED.Infrastructure.Services; public sealed class PayUPaymentService( IHttpClientFactory httpClientFactory, - IConfiguration configuration, + IIntegrationSettingsAccessor integrationSettings, ILogger logger) : IPaymentService { public async Task InitiatePaymentAsync( PaymentInitiationRequest request, CancellationToken cancellationToken = default) { - var posId = configuration["PayU:PosId"]; - var clientId = configuration["PayU:ClientId"]; - var clientSecret = configuration["PayU:ClientSecret"]; - var oauthUrl = configuration["PayU:OAuthUrl"]; - var ordersUrl = configuration["PayU:OrdersUrl"]; - var useSandbox = configuration.GetValue("PayU:UseSandbox", true); + var settings = await integrationSettings.GetAsync(cancellationToken); + var posId = settings.PayUPosId; + var clientId = settings.PayUClientId; + var clientSecret = settings.PayUClientSecret; + var oauthUrl = settings.PayUOauthUrl; + var ordersUrl = settings.PayUOrdersUrl; + var useSandbox = settings.PayUUseSandbox; var hasCredentials = !string.IsNullOrWhiteSpace(posId) && !string.IsNullOrWhiteSpace(clientId) && @@ -40,7 +41,7 @@ public sealed class PayUPaymentService( false, null, null, - "Configure PayU:PosId, ClientId, ClientSecret and URLs."); + "Configure PayU credentials in Settings → Integrations."); } var externalId = $"PAYU-{Guid.NewGuid():N}"; @@ -211,10 +212,11 @@ public sealed class PayUPaymentService( "No PayU order id to sync."); } - var clientId = configuration["PayU:ClientId"]; - var clientSecret = configuration["PayU:ClientSecret"]; - var oauthUrl = configuration["PayU:OAuthUrl"]; - var ordersUrl = configuration["PayU:OrdersUrl"]; + var settings = await integrationSettings.GetAsync(cancellationToken); + var clientId = settings.PayUClientId; + var clientSecret = settings.PayUClientSecret; + var oauthUrl = settings.PayUOauthUrl; + var ordersUrl = settings.PayUOrdersUrl; if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret) || @@ -318,20 +320,21 @@ public sealed class PayUPaymentService( } } - public Task HandleWebhookAsync( + public async Task HandleWebhookAsync( string payload, string signature, CancellationToken cancellationToken = default) { try { - var secondKey = configuration["PayU:SecondKey"]; + var settings = await integrationSettings.GetAsync(cancellationToken); + var secondKey = settings.PayUSecondKey; if (!string.IsNullOrWhiteSpace(secondKey) && !VerifySignature(payload, signature, secondKey)) { logger.LogWarning("PayU webhook signature mismatch"); - return Task.FromResult(new PaymentWebhookResult( - false, null, null, false, "Invalid signature")); + return new PaymentWebhookResult( + false, null, null, false, "Invalid signature"); } using var doc = JsonDocument.Parse(payload); @@ -362,18 +365,18 @@ public sealed class PayUPaymentService( "COMPLETED", StringComparison.OrdinalIgnoreCase); - return Task.FromResult(new PaymentWebhookResult( + return new PaymentWebhookResult( true, orderId, payuOrderId ?? extId, completed, - null)); + null); } catch (Exception ex) { logger.LogError(ex, "PayU webhook parse error"); - return Task.FromResult(new PaymentWebhookResult( - false, null, null, false, ex.Message)); + return new PaymentWebhookResult( + false, null, null, false, ex.Message); } } diff --git a/src/backend/src/iLED.Infrastructure/Services/IntegrationSettingsAccessor.cs b/src/backend/src/iLED.Infrastructure/Services/IntegrationSettingsAccessor.cs new file mode 100644 index 0000000..268f284 --- /dev/null +++ b/src/backend/src/iLED.Infrastructure/Services/IntegrationSettingsAccessor.cs @@ -0,0 +1,112 @@ +using iLED.Application.Common.Interfaces; +using iLED.Domain.Admin; +using iLED.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace iLED.Infrastructure.Services; + +public sealed class IntegrationSettingsAccessor( + ApplicationDbContext db, + IConfiguration configuration) : IIntegrationSettingsAccessor +{ + public async Task GetAsync( + CancellationToken cancellationToken = default) + { + var row = await db.StoreSettings + .AsNoTracking() + .OrderBy(s => s.CreatedAt) + .FirstOrDefaultAsync(cancellationToken); + + return From(row, configuration); + } + + internal static IntegrationRuntimeSettings From( + StoreSettings? row, + IConfiguration configuration) + { + return new IntegrationRuntimeSettings + { + ShippingUseSandbox = row?.ShippingUseSandbox + ?? configuration.GetValue("Shipping:UseSandbox", true), + CarrierInPost = row?.CarrierInPost ?? true, + CarrierDhl = row?.CarrierDhl ?? true, + CarrierDpd = row?.CarrierDpd ?? true, + InPostShipXToken = Prefer( + row?.InPostShipXToken, + configuration["Shipping:InPost:ShipXToken"]), + InPostOrganizationId = Prefer( + row?.InPostOrganizationId, + configuration["Shipping:InPost:OrganizationId"]), + InPostShipXBaseUrl = Prefer( + row?.InPostShipXBaseUrl, + configuration["Shipping:InPost:ShipXBaseUrl"]), + InPostGeowidgetPublicToken = Prefer( + row?.InPostGeowidgetPublicToken, + configuration["Shipping:InPost:GeowidgetPublicToken"]), + InPostGeowidgetSandboxToken = Prefer( + row?.InPostGeowidgetSandboxToken, + configuration["Shipping:InPost:GeowidgetSandboxToken"]), + InPostGeowidgetConfig = Prefer( + row?.InPostGeowidgetConfig, + configuration["Shipping:InPost:GeowidgetConfig"]), + InPostGeowidgetSdkBaseUrl = Prefer( + row?.InPostGeowidgetSdkBaseUrl, + configuration["Shipping:InPost:GeowidgetSdkBaseUrl"]), + DhlApiKey = Prefer( + row?.DhlApiKey, + configuration["Shipping:Dhl:ApiKey"]), + DhlApiSecret = Prefer( + row?.DhlApiSecret, + configuration["Shipping:Dhl:ApiSecret"]), + DhlApiBaseUrl = Prefer( + row?.DhlApiBaseUrl, + configuration["Shipping:Dhl:ApiBaseUrl"]), + DhlWidgetUrl = Prefer( + row?.DhlWidgetUrl, + configuration["Shipping:Dhl:WidgetUrl"]), + DpdLogin = Prefer( + row?.DpdLogin, + configuration["Shipping:Dpd:Login"]), + DpdPassword = Prefer( + row?.DpdPassword, + configuration["Shipping:Dpd:Password"]), + DpdFid = Prefer( + row?.DpdFid, + configuration["Shipping:Dpd:Fid"]), + DpdApiBaseUrl = Prefer( + row?.DpdApiBaseUrl, + configuration["Shipping:Dpd:ApiBaseUrl"]), + DpdWidgetUrl = Prefer( + row?.DpdWidgetUrl, + configuration["Shipping:Dpd:WidgetUrl"]), + PayUUseSandbox = row?.PayUUseSandbox + ?? configuration.GetValue("PayU:UseSandbox", true), + PayUPosId = Prefer(row?.PayUPosId, configuration["PayU:PosId"]), + PayUClientId = Prefer( + row?.PayUClientId, + configuration["PayU:ClientId"]), + PayUClientSecret = Prefer( + row?.PayUClientSecret, + configuration["PayU:ClientSecret"]), + PayUSecondKey = Prefer( + row?.PayUSecondKey, + configuration["PayU:SecondKey"]), + PayUOauthUrl = Prefer( + row?.PayUOauthUrl, + configuration["PayU:OAuthUrl"]), + PayUOrdersUrl = Prefer( + row?.PayUOrdersUrl, + configuration["PayU:OrdersUrl"]), + PayUNotifyUrl = Prefer( + row?.PayUNotifyUrl, + configuration["PayU:NotifyUrl"]), + PayUContinueUrl = Prefer( + row?.PayUContinueUrl, + configuration["PayU:ContinueUrl"]), + }; + } + + private static string? Prefer(string? primary, string? fallback) => + !string.IsNullOrWhiteSpace(primary) ? primary.Trim() : fallback; +} diff --git a/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs b/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs index 8cddc34..e5ae81c 100644 --- a/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs +++ b/src/backend/src/iLED.Infrastructure/Services/ShippingCarrierClients.cs @@ -12,6 +12,7 @@ namespace iLED.Infrastructure.Services; public sealed class ShippingCarrierClients( IHttpClientFactory httpClientFactory, IConfiguration configuration, + IIntegrationSettingsAccessor integrationSettings, ILogger logger, ShippingLabelStorage labelStorage) { @@ -31,14 +32,15 @@ public sealed class ShippingCarrierClients( ShipmentRequest request, CancellationToken cancellationToken) { - var token = configuration["Shipping:InPost:ShipXToken"]; - var orgId = configuration["Shipping:InPost:OrganizationId"]; - var baseUrl = configuration["Shipping:InPost:ShipXBaseUrl"] + var settings = await integrationSettings.GetAsync(cancellationToken); + var token = settings.InPostShipXToken; + var orgId = settings.InPostOrganizationId; + var baseUrl = settings.InPostShipXBaseUrl ?? "https://api-shipx-pl.easypack24.net"; if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(orgId)) { - return Fail("Configure Shipping:InPost:ShipXToken and OrganizationId."); + return Fail("Configure InPost ShipX token and organization in Settings → Integrations."); } var isLocker = request.ServiceType == ShippingServiceType.ParcelLocker; @@ -188,15 +190,16 @@ public sealed class ShippingCarrierClients( ShipmentRequest request, CancellationToken cancellationToken) { - var apiKey = configuration["Shipping:Dhl:ApiKey"]; - var apiSecret = configuration["Shipping:Dhl:ApiSecret"]; - var baseUrl = configuration["Shipping:Dhl:ApiBaseUrl"]; + var settings = await integrationSettings.GetAsync(cancellationToken); + var apiKey = settings.DhlApiKey; + var apiSecret = settings.DhlApiSecret; + var baseUrl = settings.DhlApiBaseUrl; if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(apiSecret) || string.IsNullOrWhiteSpace(baseUrl)) { - return Fail("Configure Shipping:Dhl:ApiKey, ApiSecret and ApiBaseUrl."); + return Fail("Configure DHL API key, secret and base URL in Settings → Integrations."); } var payload = new @@ -301,17 +304,18 @@ public sealed class ShippingCarrierClients( ShipmentRequest request, CancellationToken cancellationToken) { - var login = configuration["Shipping:Dpd:Login"]; - var password = configuration["Shipping:Dpd:Password"]; - var fid = configuration["Shipping:Dpd:Fid"]; - var baseUrl = configuration["Shipping:Dpd:ApiBaseUrl"]; + var settings = await integrationSettings.GetAsync(cancellationToken); + var login = settings.DpdLogin; + var password = settings.DpdPassword; + var fid = settings.DpdFid; + var baseUrl = settings.DpdApiBaseUrl; if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(fid) || string.IsNullOrWhiteSpace(baseUrl)) { - return Fail("Configure Shipping:Dpd:Login, Password, Fid and ApiBaseUrl."); + return Fail("Configure DPD login, password, FID and API URL in Settings → Integrations."); } // DPD PL PackageService-style XML envelope (simplified create). diff --git a/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs b/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs index 7431511..29ff947 100644 --- a/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs +++ b/src/backend/src/iLED.Infrastructure/Services/ShippingService.cs @@ -9,6 +9,7 @@ namespace iLED.Infrastructure.Services; public sealed class ShippingService( IConfiguration configuration, + IIntegrationSettingsAccessor integrationSettings, ILogger logger, ShippingLabelStorage labelStorage, ShippingCarrierClients carrierClients, @@ -50,16 +51,17 @@ public sealed class ShippingService( return points; } - public ShippingWidgetConfigDto GetWidgetConfig() + public async Task GetWidgetConfigAsync( + CancellationToken cancellationToken = default) { - var useSandbox = configuration.GetValue("Shipping:UseSandbox", true); + var settings = await integrationSettings.GetAsync(cancellationToken); + var useSandbox = settings.ShippingUseSandbox; var inPostToken = useSandbox - ? configuration["Shipping:InPost:GeowidgetSandboxToken"] - ?? configuration["Shipping:InPost:GeowidgetPublicToken"] - : configuration["Shipping:InPost:GeowidgetPublicToken"]; - var inPostConfig = configuration["Shipping:InPost:GeowidgetConfig"] - ?? "parcelCollect"; - var inPostSdk = configuration["Shipping:InPost:GeowidgetSdkBaseUrl"]; + ? settings.InPostGeowidgetSandboxToken + ?? settings.InPostGeowidgetPublicToken + : settings.InPostGeowidgetPublicToken; + var inPostConfig = settings.InPostGeowidgetConfig ?? "parcelCollect"; + var inPostSdk = settings.InPostGeowidgetSdkBaseUrl; if (string.IsNullOrWhiteSpace(inPostSdk)) { inPostSdk = useSandbox @@ -67,11 +69,11 @@ public sealed class ShippingService( : "https://geowidget.inpost.pl"; } - var dhlEnabled = configuration.GetValue("Shipping:Dhl:WidgetEnabled", true); - var dhlUrl = configuration["Shipping:Dhl:WidgetUrl"] + var dhlEnabled = settings.CarrierDhl; + var dhlUrl = settings.DhlWidgetUrl ?? "https://parcelshop.dhl.pl/mapa"; - var dpdEnabled = configuration.GetValue("Shipping:Dpd:WidgetEnabled", true); - var dpdUrl = configuration["Shipping:Dpd:WidgetUrl"] + var dpdEnabled = settings.CarrierDpd; + var dpdUrl = settings.DpdWidgetUrl ?? "https://pudofinder.dpd.com.pl/"; if (dpdUrl.Contains("pickup.dpd.pl", StringComparison.OrdinalIgnoreCase)) { @@ -79,9 +81,10 @@ public sealed class ShippingService( dpdUrl = "https://pudofinder.dpd.com.pl/"; } + var inPostEnabled = settings.CarrierInPost; return new ShippingWidgetConfigDto( useSandbox, - string.IsNullOrWhiteSpace(inPostToken) + !inPostEnabled || string.IsNullOrWhiteSpace(inPostToken) ? null : new InPostGeowidgetConfigDto( inPostToken,