update
This commit is contained in:
@@ -16,6 +16,7 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
|||||||
| 53 | Mobile Status Panel Toggle | 1/1 | Complete |
|
| 53 | Mobile Status Panel Toggle | 1/1 | Complete |
|
||||||
| 54 | Order Detail Image Hover | 1/1 | Complete |
|
| 54 | Order Detail Image Hover | 1/1 | Complete |
|
||||||
| 55 | Desktop Collapsed Sidebar Fix | 1/1 | Complete |
|
| 55 | Desktop Collapsed Sidebar Fix | 1/1 | Complete |
|
||||||
|
| 56 | Order Payments | 0/1 | Planning |
|
||||||
| TBD | Mobile Orders List | - | Not started |
|
| TBD | Mobile Orders List | - | Not started |
|
||||||
| TBD | Mobile Order Details | - | Not started |
|
| TBD | Mobile Order Details | - | Not started |
|
||||||
| TBD | Mobile Settings | - | Not started |
|
| TBD | Mobile Settings | - | Not started |
|
||||||
|
|||||||
@@ -5,34 +5,34 @@
|
|||||||
See: .paul/PROJECT.md (updated 2026-03-28)
|
See: .paul/PROJECT.md (updated 2026-03-28)
|
||||||
|
|
||||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||||
**Current focus:** Milestone v3.0 Mobile Responsive — Phase 52 (Mobile Main Menu) planning
|
**Current focus:** Milestone v3.0 Mobile Responsive — Phase 56 (Order Payments) planning
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.0 Mobile Responsive — In progress
|
Milestone: v3.0 Mobile Responsive — In progress
|
||||||
Phase: 4 of N (55 - Desktop Collapsed Sidebar Fix) — Complete
|
Phase: 5 of N (56 - Order Payments) — Planning
|
||||||
Plan: 55-01 complete
|
Plan: 56-01 created, awaiting approval
|
||||||
Status: Loop complete — phase 55 done, ready for next PLAN
|
Status: PLAN created, ready for APPLY
|
||||||
Last activity: 2026-03-29 — UNIFY closed for 55-01
|
Last activity: 2026-03-30 — Created .paul/phases/56-order-payments/56-01-PLAN.md
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone: [####░░░░░░] ~40%
|
- Milestone: [####░░░░░░] ~40%
|
||||||
- Phase 55: [##########] 100%
|
- Phase 56: [░░░░░░░░░░] 0%
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
✓ ○ ○ [Plan created, awaiting approval]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-29
|
Last session: 2026-03-30
|
||||||
Stopped at: Phase 55 complete
|
Stopped at: Plan 56-01 created
|
||||||
Next action: /paul:plan dla kolejnego modulu
|
Next action: Review and approve plan, then run /paul:apply .paul/phases/56-order-payments/56-01-PLAN.md
|
||||||
Resume file: .paul/phases/55-desktop-collapsed-sidebar-fix/55-01-SUMMARY.md
|
Resume file: .paul/phases/56-order-payments/56-01-PLAN.md
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -41,9 +41,10 @@ Resume file: .paul/phases/55-desktop-collapsed-sidebar-fix/55-01-SUMMARY.md
|
|||||||
|------|----------|--------|
|
|------|----------|--------|
|
||||||
| 2026-03-29 | Mobile menu jako slide-in overlay (nie horizontal scroll) | Pelna nawigacja na mobile bez kompromisow |
|
| 2026-03-29 | Mobile menu jako slide-in overlay (nie horizontal scroll) | Pelna nawigacja na mobile bez kompromisow |
|
||||||
| 2026-03-29 | Hamburger w topbarze, sidebar fixed z transform slide | Plynna animacja CSS, zero zaleznosci JS |
|
| 2026-03-29 | Hamburger w topbarze, sidebar fixed z transform slide | Plynna animacja CSS, zero zaleznosci JS |
|
||||||
|
| 2026-03-30 | Push set_paid do shopPRO API po dodaniu platnosci w orderPRO | Synchronizacja statusu platnosci bez dodatkowego endpointu w shopPRO |
|
||||||
|
|
||||||
## Git State
|
## Git State
|
||||||
|
|
||||||
Last commit: cbc2058
|
Last commit: 70662af
|
||||||
Branch: main
|
Branch: main
|
||||||
Feature branches merged: none
|
Feature branches merged: none
|
||||||
|
|||||||
248
.paul/phases/56-order-payments/56-01-PLAN.md
Normal file
248
.paul/phases/56-order-payments/56-01-PLAN.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
---
|
||||||
|
phase: 56-order-payments
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260330_000073_create_order_payments_table.sql
|
||||||
|
- src/Modules/Orders/OrdersController.php
|
||||||
|
- src/Modules/Orders/OrdersRepository.php
|
||||||
|
- src/Modules/Settings/ShopproApiClient.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/modules/_order-details.scss
|
||||||
|
- public/assets/css/app.css
|
||||||
|
- routes/web.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
autonomous: false
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Uruchomienie funkcji dodawania platnosci do zamowienia z poziomu zakladki Platnosci w widoku zamowienia. Po dodaniu platnosci — automatyczny push statusu platnosci do shopPRO API (dla zamowien source=shoppro).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Sprzedawca moze oznaczyc zamowienie jako oplacone bezposrednio z orderPRO i ta informacja jest synchronizowana do sklepu shopPRO.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Tabela `order_payments` w bazie (migracja)
|
||||||
|
- Formularz dodawania platnosci w zakladce Platnosci
|
||||||
|
- Endpoint POST `/orders/{id}/payment/add`
|
||||||
|
- Push `set_paid` do shopPRO API po dodaniu platnosci (source=shoppro)
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@database/drafts/20260302_orders_schema_v1.sql (linie 102-121 — draft schemat order_payments)
|
||||||
|
@src/Modules/Orders/OrdersController.php
|
||||||
|
@src/Modules/Orders/OrdersRepository.php (loadOrderPayments, findDetails)
|
||||||
|
@src/Modules/Orders/OrderImportRepository.php (replacePayments)
|
||||||
|
@src/Modules/Settings/ShopproPaymentStatusSyncService.php (wzorzec update + push)
|
||||||
|
@src/Modules/Settings/ShopproApiClient.php (requestJsonPut)
|
||||||
|
@src/Modules/Settings/ShopproIntegrationsRepository.php
|
||||||
|
@resources/views/orders/show.php (zakladka Platnosci linie 559-648)
|
||||||
|
@routes/web.php (pattern: /orders/{id}/...)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Tabela order_payments istnieje w bazie
|
||||||
|
```gherkin
|
||||||
|
Given migracja 20260330_000073 zostala uruchomiona
|
||||||
|
When wykonam SHOW CREATE TABLE order_payments
|
||||||
|
Then tabela zawiera kolumny: id, order_id, source_payment_id, external_payment_id, payment_type_id, payment_date, amount, currency, comment, payload_json, created_at, updated_at
|
||||||
|
And istnieje FK na orders(id) z ON DELETE CASCADE
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Formularz dodawania platnosci w zakladce Platnosci
|
||||||
|
```gherkin
|
||||||
|
Given otwieram zamowienie /orders/130
|
||||||
|
When klikam zakladke Platnosci
|
||||||
|
Then widze przycisk "Dodaj platnosc" nad tabela platnosci
|
||||||
|
And po kliknieciu pojawia sie formularz inline z polami: kwota, typ platnosci (select), data platnosci, komentarz
|
||||||
|
And formularz zawiera przyciski Zapisz i Anuluj
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Zapis platnosci przez AJAX
|
||||||
|
```gherkin
|
||||||
|
Given wypelniam formularz platnosci (kwota, typ, data)
|
||||||
|
When klikam Zapisz
|
||||||
|
Then platnosc jest zapisana w tabeli order_payments
|
||||||
|
And kolumny orders.payment_status i orders.total_paid sa zaktualizowane
|
||||||
|
And zakladka Platnosci odswieza sie z nowa platnoscia na liscie
|
||||||
|
And pojawia sie komunikat sukcesu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Push set_paid do shopPRO po dodaniu platnosci
|
||||||
|
```gherkin
|
||||||
|
Given zamowienie ma source=shoppro i polaczona integracje
|
||||||
|
When dodaje platnosc pokrywajaca pelna kwote zamowienia
|
||||||
|
Then orderPRO wywoluje PUT shopPRO API /api.php?endpoint=orders&action=set_paid&id={source_order_id}
|
||||||
|
And w activity_log zapisuje informacje o pushu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Walidacja formularza
|
||||||
|
```gherkin
|
||||||
|
Given otwieram formularz dodawania platnosci
|
||||||
|
When probuje zapisac bez kwoty lub z kwota <= 0
|
||||||
|
Then formularz nie jest wysylany i pojawia sie komunikat bledu walidacji
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Migracja order_payments + update orders columns</name>
|
||||||
|
<files>database/migrations/20260330_000073_create_order_payments_table.sql, DOCS/DB_SCHEMA.md</files>
|
||||||
|
<action>
|
||||||
|
Utworz migracje tworzaca tabele order_payments na podstawie draftu (database/drafts/20260302_orders_schema_v1.sql linie 102-121).
|
||||||
|
Uzyj CREATE TABLE IF NOT EXISTS — migracja idempotentna.
|
||||||
|
Schemat: id, order_id, source_payment_id, external_payment_id, payment_type_id, payment_date, amount, currency, comment, payload_json, created_at, updated_at.
|
||||||
|
Indexy: UNIQUE (order_id, source_payment_id), INDEX (order_id), INDEX (payment_date).
|
||||||
|
FK: order_payments_order_fk -> orders(id) ON DELETE CASCADE ON UPDATE CASCADE.
|
||||||
|
|
||||||
|
Sprawdz tez czy kolumny payment_status, total_paid, external_payment_type_id istnieja w tabeli orders. Jezeli nie — dodaj ALTER TABLE (idempotentnie przez IF NOT EXISTS pattern z prepared statements).
|
||||||
|
|
||||||
|
Zaktualizuj DOCS/DB_SCHEMA.md o nowa tabele.
|
||||||
|
</action>
|
||||||
|
<verify>Uruchom migracje na serwerze: sprawdz SHOW CREATE TABLE order_payments</verify>
|
||||||
|
<done>AC-1 satisfied: tabela order_payments istnieje z poprawnym schematem</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Backend — endpoint dodawania platnosci + push do shopPRO</name>
|
||||||
|
<files>src/Modules/Orders/OrdersController.php, src/Modules/Orders/OrdersRepository.php, src/Modules/Settings/ShopproApiClient.php, routes/web.php, resources/lang/pl.php, DOCS/ARCHITECTURE.md</files>
|
||||||
|
<action>
|
||||||
|
1. W routes/web.php dodaj route:
|
||||||
|
POST /orders/{id}/payment/add -> OrdersController->addPayment
|
||||||
|
|
||||||
|
2. W OrdersRepository dodaj metode addPayment(int $orderId, array $data): int
|
||||||
|
- INSERT INTO order_payments (order_id, payment_type_id, payment_date, amount, currency, comment)
|
||||||
|
- Przelicz total_paid: SELECT SUM(amount) FROM order_payments WHERE order_id
|
||||||
|
- UPDATE orders SET total_paid = sum, payment_status = (2 jesli sum >= total_with_tax, 1 jesli sum > 0, 0 jesli sum = 0)
|
||||||
|
- Zwroc nowy payment ID
|
||||||
|
|
||||||
|
3. W OrdersController dodaj metode addPayment(Request $request): Response
|
||||||
|
- Walidacja: orderId > 0, amount > 0, payment_type_id niepusty
|
||||||
|
- CSRF token
|
||||||
|
- Wywolaj OrdersRepository::addPayment()
|
||||||
|
- Jezeli zamowienie ma source=shoppro i payment_status stalo sie 2 (oplacone):
|
||||||
|
- Pobierz dane integracji (integration_id z zamowienia)
|
||||||
|
- Wywolaj ShopproApiClient::pushPaymentPaid() do set_paid
|
||||||
|
- Zaloguj w activity_log
|
||||||
|
- Return JSON response {ok, payment, payment_status, total_paid}
|
||||||
|
|
||||||
|
4. W ShopproApiClient dodaj metode pushPaymentPaid(string $baseUrl, string $apiKey, int $timeout, string $sourceOrderId): array
|
||||||
|
- PUT {baseUrl}/api.php?endpoint=orders&action=set_paid&id={sourceOrderId}
|
||||||
|
- Body: {"send_email": 0}
|
||||||
|
- Uzyj istniejacego requestJsonPut()
|
||||||
|
|
||||||
|
5. Zaktualizuj resources/lang/pl.php o klucze bledow/komunikatow platnosci.
|
||||||
|
|
||||||
|
6. Zaktualizuj DOCS/ARCHITECTURE.md.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
curl -X POST /orders/130/payment/add z danymi platnosci — odpowiedz 200 z JSON
|
||||||
|
</verify>
|
||||||
|
<done>AC-3, AC-4, AC-5 satisfied: platnosc zapisywana, push do shopPRO, walidacja</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Frontend — formularz platnosci w zakladce + AJAX</name>
|
||||||
|
<files>resources/views/orders/show.php, resources/scss/modules/_order-details.scss, public/assets/css/app.css</files>
|
||||||
|
<action>
|
||||||
|
1. W show.php, w sekcji data-order-tab-panel="payments" (linia ~606):
|
||||||
|
- Dodaj przycisk "Dodaj platnosc" (klasa btn btn--sm btn--primary) nad tabela/komunikatem "Brak platnosci"
|
||||||
|
- Po kliknieciu — pokaz formularz inline (ukryty domyslnie):
|
||||||
|
- Kwota (input number, step=0.01, required) — domyslna: total_with_tax - total_paid
|
||||||
|
- Typ platnosci (select: ONLINE, TRANSFER, CASH_ON_DELIVERY)
|
||||||
|
- Data platnosci (input date, domyslna: dzisiaj)
|
||||||
|
- Komentarz (input text, opcjonalny)
|
||||||
|
- Przyciski: Zapisz (submit AJAX), Anuluj (ukryj formularz)
|
||||||
|
|
||||||
|
2. JavaScript na dole widoku (pattern jak inne formularze AJAX w tym pliku):
|
||||||
|
- POST /orders/{orderId}/payment/add
|
||||||
|
- Wysylaj JSON: {amount, payment_type_id, payment_date, comment, _token}
|
||||||
|
- On success: window.location.reload() dla odswiezenia karty
|
||||||
|
- On error: pokaz blad przez OrderProAlerts.error()
|
||||||
|
|
||||||
|
3. Style w _order-details.scss:
|
||||||
|
- .payment-add-form — kompaktowy formularz inline
|
||||||
|
- .payment-add-form__row — flex row z gap
|
||||||
|
- Responsywnosc (na mobile kolumny zawijaja sie)
|
||||||
|
|
||||||
|
4. Zbuduj SCSS: npm run build (lub sass compile)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Otworz /orders/130, kliknij Platnosci, kliknij "Dodaj platnosc", wypelnij formularz, zapisz — platnosc pojawia sie na liscie
|
||||||
|
</verify>
|
||||||
|
<done>AC-2, AC-3 satisfied: formularz widoczny, zapis dziala, lista odswieza sie</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Formularz dodawania platnosci + push do shopPRO</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
1. Otworz: https://orderpro.projectpro.pl/orders/130
|
||||||
|
2. Kliknij zakladke "Platnosci"
|
||||||
|
3. Kliknij "Dodaj platnosc"
|
||||||
|
4. Wypelnij kwote (np. 100.00), wybierz typ "Platnosc online", data dzisiejsza
|
||||||
|
5. Kliknij "Zapisz"
|
||||||
|
6. Sprawdz: platnosc pojawia sie w tabeli, status platnosci zaktualizowal sie
|
||||||
|
7. Sprawdz shopPRO: czy zamowienie zostalo oznaczone jako oplacone
|
||||||
|
8. Sprobuj dodac platnosc z kwota 0 — powinien byc blad walidacji
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- src/Modules/Settings/ShopproPaymentStatusSyncService.php (istniejacy cron sync — nie modyfikowac)
|
||||||
|
- src/Modules/Orders/OrderImportRepository.php (import platnosci z API — nie modyfikowac)
|
||||||
|
- database/migrations/ istniejace migracje (nie modyfikowac)
|
||||||
|
- src/Modules/Cron/* (nie modyfikowac handlerow crona)
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Tylko dodawanie platnosci — nie edycja ani usuwanie
|
||||||
|
- Nie implementujemy zwrotow (refunds)
|
||||||
|
- Push do shopPRO tylko przez istniejace API set_paid — nie dodajemy nowego endpointu w shopPRO
|
||||||
|
- Nie modyfikujemy synchronizacji platnosci Allegro
|
||||||
|
- Nie dodajemy nowych cron jobow
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Migracja uruchomiona — tabela order_payments istnieje
|
||||||
|
- [ ] POST /orders/{id}/payment/add zwraca 200 z poprawnymi danymi
|
||||||
|
- [ ] Formularz w UI wyswietla sie poprawnie
|
||||||
|
- [ ] Platnosc zapisana w order_payments
|
||||||
|
- [ ] orders.payment_status i total_paid zaktualizowane
|
||||||
|
- [ ] Push set_paid do shopPRO (source=shoppro) dziala
|
||||||
|
- [ ] Walidacja formularza — kwota > 0, typ niepusty
|
||||||
|
- [ ] DOCS zaktualizowane
|
||||||
|
- [ ] SCSS zbudowany do CSS
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Wszystkie taski ukonczone
|
||||||
|
- Wszystkie AC spelnione
|
||||||
|
- Platnosc dodana z UI pojawia sie w zakladce Platnosci
|
||||||
|
- shopPRO otrzymuje informacje o oplaceniu
|
||||||
|
- Brak bledow PHP/JS w konsoli
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/56-order-payments/56-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
36
.vscode/ftp-kr.sync.cache.json
vendored
36
.vscode/ftp-kr.sync.cache.json
vendored
@@ -1824,8 +1824,8 @@
|
|||||||
"css": {
|
"css": {
|
||||||
"app.css": {
|
"app.css": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 45416,
|
"size": 57250,
|
||||||
"lmtime": 1774702916830,
|
"lmtime": 1774820920578,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"app.css.map": {
|
"app.css.map": {
|
||||||
@@ -1897,8 +1897,8 @@
|
|||||||
"lang": {
|
"lang": {
|
||||||
"pl.php": {
|
"pl.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 62881,
|
"size": 62845,
|
||||||
"lmtime": 1773788074010,
|
"lmtime": 1774861982010,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1933,8 +1933,8 @@
|
|||||||
},
|
},
|
||||||
"app.scss": {
|
"app.scss": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 43784,
|
"size": 45727,
|
||||||
"lmtime": 1774701658193,
|
"lmtime": 1774820913052,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"login.css": {
|
"login.css": {
|
||||||
@@ -2013,17 +2013,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"table-list.php": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 21805,
|
|
||||||
"lmtime": 1771925480312,
|
|
||||||
"modified": false
|
|
||||||
},
|
|
||||||
"order-status-panel.php": {
|
"order-status-panel.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 1743,
|
"size": 2682,
|
||||||
"lmtime": 1772497361423,
|
"lmtime": 1774819455980,
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
"table-list.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 21914,
|
||||||
|
"lmtime": 1771925480312,
|
||||||
|
"modified": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
@@ -2037,8 +2037,8 @@
|
|||||||
"layouts": {
|
"layouts": {
|
||||||
"app.php": {
|
"app.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 9591,
|
"size": 11645,
|
||||||
"lmtime": 1774566330881,
|
"lmtime": 1774818596878,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"auth.php": {
|
"auth.php": {
|
||||||
@@ -2091,8 +2091,8 @@
|
|||||||
},
|
},
|
||||||
"show.php": {
|
"show.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 40747,
|
"size": 41726,
|
||||||
"lmtime": 1774474658482,
|
"lmtime": 1774820344044,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
- `POST /orders/{id}/status`
|
- `POST /orders/{id}/status`
|
||||||
- `POST /orders/{id}/send-email` (wysylka e-mail z zamowienia, AJAX)
|
- `POST /orders/{id}/send-email` (wysylka e-mail z zamowienia, AJAX)
|
||||||
- `POST /orders/{id}/email-preview` (podglad szablonu z rozwiazanymi zmiennymi, AJAX)
|
- `POST /orders/{id}/email-preview` (podglad szablonu z rozwiazanymi zmiennymi, AJAX)
|
||||||
|
- `POST /orders/{id}/payment/add` (dodanie platnosci recznej, AJAX + push set_paid do shopPRO)
|
||||||
- `GET /accounting` (lista paragonow z filtrami i paginacja)
|
- `GET /accounting` (lista paragonow z filtrami i paginacja)
|
||||||
- `GET /accounting/export` (eksport XLSX z aktywnymi filtrami)
|
- `GET /accounting/export` (eksport XLSX z aktywnymi filtrami)
|
||||||
- `GET /users` (redirect do `/settings/users`)
|
- `GET /users` (redirect do `/settings/users`)
|
||||||
|
|||||||
@@ -223,6 +223,26 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
|||||||
- `allegro_order_status_mappings_code_unique` (UNIQUE: `allegro_status_code`),
|
- `allegro_order_status_mappings_code_unique` (UNIQUE: `allegro_status_code`),
|
||||||
- `allegro_order_status_mappings_orderpro_code_idx` (`orderpro_status_code`).
|
- `allegro_order_status_mappings_orderpro_code_idx` (`orderpro_status_code`).
|
||||||
|
|
||||||
|
### `order_payments`
|
||||||
|
- Platnosci zamowien (z importu API lub reczne).
|
||||||
|
- Kolumny:
|
||||||
|
- `id` (PK, bigint unsigned, AI),
|
||||||
|
- `order_id` (bigint unsigned, FK -> `orders.id`, CASCADE),
|
||||||
|
- `source_payment_id` (varchar 64, nullable),
|
||||||
|
- `external_payment_id` (varchar 64, nullable),
|
||||||
|
- `payment_type_id` (varchar 64, NOT NULL) — typ: ONLINE, TRANSFER, CASH_ON_DELIVERY,
|
||||||
|
- `payment_date` (datetime, nullable),
|
||||||
|
- `amount` (decimal 12,2, nullable),
|
||||||
|
- `currency` (char 3, nullable),
|
||||||
|
- `comment` (varchar 255, nullable),
|
||||||
|
- `payload_json` (json, nullable),
|
||||||
|
- `created_at`, `updated_at`.
|
||||||
|
- Indeksy:
|
||||||
|
- `order_payments_order_source_payment_unique` (UNIQUE: `order_id`, `source_payment_id`),
|
||||||
|
- `order_payments_order_idx` (`order_id`),
|
||||||
|
- `order_payments_date_idx` (`payment_date`).
|
||||||
|
- Migracja: `20260330_000073_create_order_payments_table.sql`
|
||||||
|
|
||||||
### `order_activity_log`
|
### `order_activity_log`
|
||||||
- Uniwersalny log aktywnosci zamowienia (zmiany statusow, platnosci, przesylki, faktury, wiadomosci itp.).
|
- Uniwersalny log aktywnosci zamowienia (zmiany statusow, platnosci, przesylki, faktury, wiadomosci itp.).
|
||||||
- Kolumny:
|
- Kolumny:
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# Tech Changelog
|
# Tech Changelog
|
||||||
|
|
||||||
|
## 2026-03-30 (Phase 56 - Order Payments, Plan 01)
|
||||||
|
- Migracja `20260330_000073_create_order_payments_table.sql`: tabela `order_payments` (id, order_id, source_payment_id, external_payment_id, payment_type_id, payment_date, amount, currency, comment, payload_json) + idempotentne dodanie kolumn `total_with_tax`, `total_paid`, `external_payment_type_id` do `orders`.
|
||||||
|
- `OrdersRepository::addPayment()`: INSERT do `order_payments`, przeliczenie `total_paid` i `payment_status` na `orders`.
|
||||||
|
- `OrdersRepository::findOrderSourceInfo()`: pobiera `source`, `integration_id`, `source_order_id` dla push do shopPRO.
|
||||||
|
- `OrdersController::addPayment()`: POST `/orders/{id}/payment/add` — walidacja, zapis platnosci, activity log, push do shopPRO.
|
||||||
|
- `OrdersController::pushPaymentToShoppro()`: po dodaniu platnosci pokrywajacej calosc — PUT `set_paid` do shopPRO API.
|
||||||
|
- `ShopproApiClient::setOrderPaid()`: PUT `/api.php?endpoint=orders&action=set_paid&id={sourceOrderId}`.
|
||||||
|
- `resources/views/orders/show.php`: przycisk "Dodaj platnosc", formularz inline (kwota, typ, data, komentarz), AJAX submit z reload.
|
||||||
|
- Style: `.payment-add-form` w `resources/scss/app.scss`.
|
||||||
|
|
||||||
## 2026-03-28 (Phase 51 - Email HTML Layout, Plan 01)
|
## 2026-03-28 (Phase 51 - Email HTML Layout, Plan 01)
|
||||||
- Migracja `20260328_000001_add_html_layout_to_email_mailboxes.sql`: kolumny `header_html` TEXT NULL i `footer_html` TEXT NULL w `email_mailboxes`.
|
- Migracja `20260328_000001_add_html_layout_to_email_mailboxes.sql`: kolumny `header_html` TEXT NULL i `footer_html` TEXT NULL w `email_mailboxes`.
|
||||||
- `EmailMailboxRepository::save()`: zapis `header_html`/`footer_html` w INSERT i UPDATE.
|
- `EmailMailboxRepository::save()`: zapis `header_html`/`footer_html` w INSERT i UPDATE.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
CREATE TABLE IF NOT EXISTS allegro_integration_settings (
|
CREATE TABLE IF NOT EXISTS allegro_integration_settings (
|
||||||
id TINYINT UNSIGNED NOT NULL PRIMARY KEY,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
environment VARCHAR(16) NOT NULL DEFAULT 'sandbox',
|
environment VARCHAR(16) NOT NULL DEFAULT 'sandbox',
|
||||||
client_id VARCHAR(128) NULL,
|
client_id VARCHAR(128) NULL,
|
||||||
client_secret_encrypted TEXT NULL,
|
client_secret_encrypted TEXT NULL,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- Migracja: Tabela order_payments
|
||||||
|
-- Cel: Przechowywanie platnosci zamowien (recznych i z importu)
|
||||||
|
-- Zrodlo schematu: database/drafts/20260302_orders_schema_v1.sql
|
||||||
|
-- Idempotentna: CREATE TABLE IF NOT EXISTS
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS order_payments (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
source_payment_id VARCHAR(64) NULL,
|
||||||
|
external_payment_id VARCHAR(64) NULL,
|
||||||
|
payment_type_id VARCHAR(64) NOT NULL,
|
||||||
|
payment_date DATETIME NULL,
|
||||||
|
amount DECIMAL(12,2) NULL,
|
||||||
|
currency CHAR(3) NULL,
|
||||||
|
comment VARCHAR(255) NULL,
|
||||||
|
payload_json JSON NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY order_payments_order_source_payment_unique (order_id, source_payment_id),
|
||||||
|
KEY order_payments_order_idx (order_id),
|
||||||
|
KEY order_payments_date_idx (payment_date),
|
||||||
|
CONSTRAINT order_payments_order_fk
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Kolumny payment w orders — idempotentne dodanie (mogly byc juz zaladowane z deploy scriptu)
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `orders` ADD COLUMN `total_with_tax` DECIMAL(12,2) NULL AFTER `total_net`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'orders'
|
||||||
|
AND column_name = 'total_with_tax'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `orders` ADD COLUMN `total_paid` DECIMAL(12,2) NULL AFTER `total_with_tax`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'orders'
|
||||||
|
AND column_name = 'total_paid'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := (
|
||||||
|
SELECT IF(COUNT(*) = 0,
|
||||||
|
'ALTER TABLE `orders` ADD COLUMN `external_payment_type_id` VARCHAR(128) NULL AFTER `payment_status`',
|
||||||
|
'SELECT 1')
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'orders'
|
||||||
|
AND column_name = 'external_payment_type_id'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -545,6 +545,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2355,6 +2356,45 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-add-form {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-add-form__row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-add-form__field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.payment-add-form__field label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.payment-add-form__field input, .payment-add-form__field select {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-add-form__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.order-kv dt {
|
.order-kv dt {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ return [
|
|||||||
'orders' => 'Zamowienia',
|
'orders' => 'Zamowienia',
|
||||||
'orders_list' => 'Lista zamowien',
|
'orders_list' => 'Lista zamowien',
|
||||||
'marketplace' => 'Marketplace',
|
'marketplace' => 'Marketplace',
|
||||||
'cron' => 'Cron',
|
'cron' => 'Harmonogram',
|
||||||
'dashboard' => 'Dashboard',
|
'dashboard' => 'Dashboard',
|
||||||
'settings' => 'Ustawienia',
|
'settings' => 'Ustawienia',
|
||||||
'statuses' => 'Statusy',
|
'statuses' => 'Statusy',
|
||||||
@@ -1038,7 +1038,7 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'cron' => [
|
'cron' => [
|
||||||
'title' => 'Cron',
|
'title' => 'Harmonogram',
|
||||||
'run_on_web_title' => 'Uruchamianie crona podczas nawigacji',
|
'run_on_web_title' => 'Uruchamianie crona podczas nawigacji',
|
||||||
'run_on_web_description' => 'Po wlaczeniu worker cron uruchamia sie automatycznie podczas poruszania po panelu.',
|
'run_on_web_description' => 'Po wlaczeniu worker cron uruchamia sie automatycznie podczas poruszania po panelu.',
|
||||||
'run_on_web_label' => 'Wlacz uruchamianie crona podczas requestow HTTP',
|
'run_on_web_label' => 'Wlacz uruchamianie crona podczas requestow HTTP',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1645,6 +1645,47 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-add-form {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-add-form__row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-add-form__field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 120px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-add-form__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.order-kv dt {
|
.order-kv dt {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -444,12 +444,20 @@ foreach ($addressesList as $address) {
|
|||||||
<?php if ($isManual): ?>
|
<?php if ($isManual): ?>
|
||||||
<span class="order-tag is-neutral">Dodana recznie</span>
|
<span class="order-tag is-neutral">Dodana recznie</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="order-tag <?= $pkgStatus === 'label_ready' || $pkgStatus === 'created' ? 'is-success' : ($pkgStatus === 'error' ? 'is-danger' : 'is-warn') ?>">
|
<span class="order-tag <?= $pkgStatus === 'label_ready' || $pkgStatus === 'created' ? 'is-success' : ($pkgStatus === 'error' ? 'is-danger' : 'is-warn') ?>"
|
||||||
|
data-pkg-status-tag="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
|
||||||
<?= $e($pkgStatus) ?>
|
<?= $e($pkgStatus) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<?php if ($pkgStatus === 'pending'): ?>
|
||||||
|
<button type="button" class="btn btn--xs btn--secondary mt-4"
|
||||||
|
data-check-pkg-status="<?= $e((string) ($pkg['id'] ?? 0)) ?>"
|
||||||
|
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
|
||||||
|
data-auto-poll="1"
|
||||||
|
style="font-size:0.7rem">Sprawdz status</button>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($pkgError !== ''): ?>
|
<?php if ($pkgError !== ''): ?>
|
||||||
<div class="muted mt-4" style="font-size:0.75rem"><?= $e($pkgError) ?></div>
|
<div class="muted mt-4" style="font-size:0.75rem" data-pkg-error="<?= $e((string) ($pkg['id'] ?? 0)) ?>"><?= $e($pkgError) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -462,13 +470,13 @@ foreach ($addressesList as $address) {
|
|||||||
?>
|
?>
|
||||||
<span class="delivery-badge delivery-badge--<?= $e($pkgDeliveryStatus) ?>" title="<?= $e($pkgDeliveryTitle) ?>"><?= $e($pkgDeliveryLabel) ?></span>
|
<span class="delivery-badge delivery-badge--<?= $e($pkgDeliveryStatus) ?>" title="<?= $e($pkgDeliveryTitle) ?>"><?= $e($pkgDeliveryLabel) ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td style="white-space:nowrap">
|
<td style="white-space:nowrap" data-pkg-tracking-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
|
||||||
<?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php
|
<?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php
|
||||||
$pkgTrackUrl = \App\Modules\Shipments\DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, $pkgCarrierId);
|
$pkgTrackUrl = \App\Modules\Shipments\DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, $pkgCarrierId);
|
||||||
if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesylke">🔗</a><?php endif; ?>
|
if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesylke">🔗</a><?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?php if ($isManual): ?><?= $e($pkgCarrierId !== '' ? $pkgCarrierId : 'Reczna') ?><?php elseif ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> → <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
|
<td><?php if ($isManual): ?><?= $e($pkgCarrierId !== '' ? $pkgCarrierId : 'Reczna') ?><?php elseif ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> → <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
|
||||||
<td>
|
<td data-pkg-label-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
|
||||||
<?php if ($isManual): ?>
|
<?php if ($isManual): ?>
|
||||||
—
|
—
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -595,8 +603,42 @@ foreach ($addressesList as $address) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<button type="button" class="btn btn--sm btn--primary" id="btn-add-payment">+ Dodaj płatność</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="payment-add-form mt-12" id="payment-add-form" style="display:none">
|
||||||
|
<div class="payment-add-form__row">
|
||||||
|
<div class="payment-add-form__field">
|
||||||
|
<label for="payment-amount">Kwota</label>
|
||||||
|
<input type="number" id="payment-amount" step="0.01" min="0.01" required
|
||||||
|
value="<?= $e(number_format(max(0, (float) ($orderRow['total_with_tax'] ?? 0) - (float) ($orderRow['total_paid'] ?? 0)), 2, '.', '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="payment-add-form__field">
|
||||||
|
<label for="payment-type">Typ płatności</label>
|
||||||
|
<select id="payment-type" required>
|
||||||
|
<option value="ONLINE">Płatność online</option>
|
||||||
|
<option value="TRANSFER">Przelew bankowy</option>
|
||||||
|
<option value="CASH_ON_DELIVERY">Za pobraniem</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="payment-add-form__field">
|
||||||
|
<label for="payment-date">Data</label>
|
||||||
|
<input type="date" id="payment-date" value="<?= $e(date('Y-m-d')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="payment-add-form__field">
|
||||||
|
<label for="payment-comment">Komentarz</label>
|
||||||
|
<input type="text" id="payment-comment" placeholder="opcjonalny">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="payment-add-form__actions mt-8">
|
||||||
|
<button type="button" class="btn btn--sm btn--primary" id="btn-save-payment">Zapisz</button>
|
||||||
|
<button type="button" class="btn btn--sm btn--outline" id="btn-cancel-payment">Anuluj</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if ($paymentsList === []): ?>
|
<?php if ($paymentsList === []): ?>
|
||||||
<p class="muted mt-16">Brak zarejestrowanych płatności.</p>
|
<p class="muted mt-16" id="payments-empty-msg">Brak zarejestrowanych płatności.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="table-wrap mt-16">
|
<div class="table-wrap mt-16">
|
||||||
<table class="table table--details">
|
<table class="table table--details">
|
||||||
@@ -739,41 +781,41 @@ foreach ($addressesList as $address) {
|
|||||||
try { savedTab = localStorage.getItem(storageKey); } catch (e) {}
|
try { savedTab = localStorage.getItem(storageKey); } catch (e) {}
|
||||||
setActiveTab(forceTab || savedTab || 'details');
|
setActiveTab(forceTab || savedTab || 'details');
|
||||||
|
|
||||||
// Print label button handler
|
// Print label button handler (delegated for dynamically added buttons)
|
||||||
document.querySelectorAll('.btn-print-label').forEach(function (btn) {
|
document.addEventListener('click', function (e) {
|
||||||
btn.addEventListener('click', function () {
|
var btn = e.target.closest('.btn-print-label');
|
||||||
var packageId = btn.getAttribute('data-package-id');
|
if (!btn) return;
|
||||||
if (!packageId) return;
|
var packageId = btn.getAttribute('data-package-id');
|
||||||
btn.disabled = true;
|
if (!packageId) return;
|
||||||
var originalText = btn.innerHTML;
|
btn.disabled = true;
|
||||||
btn.innerHTML = 'Wysylam...';
|
var originalText = btn.innerHTML;
|
||||||
var csrfInput = document.querySelector('input[name="_token"]');
|
btn.innerHTML = 'Wysylam...';
|
||||||
var csrf = csrfInput ? csrfInput.value : '<?= $e($csrfToken ?? '') ?>';
|
var csrfInput = document.querySelector('input[name="_token"]');
|
||||||
|
var csrf = csrfInput ? csrfInput.value : '<?= $e($csrfToken ?? '') ?>';
|
||||||
|
|
||||||
fetch('/api/print/jobs', {
|
fetch('/api/print/jobs', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: '_token=' + encodeURIComponent(csrf) + '&package_id=' + encodeURIComponent(packageId)
|
body: '_token=' + encodeURIComponent(csrf) + '&package_id=' + encodeURIComponent(packageId)
|
||||||
})
|
})
|
||||||
.then(function (r) { return r.json().then(function (d) { return { status: r.status, data: d }; }); })
|
.then(function (r) { return r.json().then(function (d) { return { status: r.status, data: d }; }); })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (res.status === 201 || res.status === 409) {
|
if (res.status === 201 || res.status === 409) {
|
||||||
btn.innerHTML = 'W kolejce';
|
btn.innerHTML = 'W kolejce';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.remove('btn--secondary');
|
btn.classList.remove('btn--secondary');
|
||||||
btn.classList.add('btn--danger');
|
btn.classList.add('btn--danger');
|
||||||
} else {
|
} else {
|
||||||
var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany blad';
|
var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany blad';
|
||||||
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: msg, type: 'error' }); }
|
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: msg, type: 'error' }); }
|
||||||
btn.innerHTML = originalText;
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: 'Blad sieci.', type: 'error' }); }
|
|
||||||
btn.innerHTML = originalText;
|
btn.innerHTML = originalText;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: 'Blad sieci.', type: 'error' }); }
|
||||||
|
btn.innerHTML = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -805,4 +847,184 @@ foreach ($addressesList as $address) {
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var csrfToken = (document.querySelector('input[name="_token"]') || {}).value || '';
|
||||||
|
|
||||||
|
function updateRowSuccess(pkgId, orderId, data) {
|
||||||
|
var tag = document.querySelector('[data-pkg-status-tag="' + pkgId + '"]');
|
||||||
|
if (tag) {
|
||||||
|
tag.className = 'order-tag is-success';
|
||||||
|
tag.textContent = data.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = document.querySelector('[data-check-pkg-status="' + pkgId + '"]');
|
||||||
|
if (btn) btn.remove();
|
||||||
|
|
||||||
|
var trackingCell = document.querySelector('[data-pkg-tracking-cell="' + pkgId + '"]');
|
||||||
|
if (trackingCell && data.tracking_number) {
|
||||||
|
trackingCell.textContent = data.tracking_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
var labelCell = document.querySelector('[data-pkg-label-cell="' + pkgId + '"]');
|
||||||
|
if (labelCell) {
|
||||||
|
var html = '<span style="display:inline-flex;gap:4px;align-items:center">';
|
||||||
|
if (data.status === 'label_ready') {
|
||||||
|
html += '<form method="post" action="/orders/' + orderId + '/shipment/' + pkgId + '/label" style="display:inline">'
|
||||||
|
+ '<input type="hidden" name="_token" value="' + csrfToken + '">'
|
||||||
|
+ '<button type="submit" class="btn btn--sm btn--secondary">Pobierz</button></form>';
|
||||||
|
}
|
||||||
|
html += '<button type="button" class="btn btn--sm btn--secondary btn-print-label"'
|
||||||
|
+ ' data-package-id="' + pkgId + '" data-order-id="' + orderId + '" title="Wyslij do drukarki">Drukuj</button>';
|
||||||
|
html += '</span>';
|
||||||
|
labelCell.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRowError(pkgId, errorMsg) {
|
||||||
|
var tag = document.querySelector('[data-pkg-status-tag="' + pkgId + '"]');
|
||||||
|
if (tag) {
|
||||||
|
tag.className = 'order-tag is-danger';
|
||||||
|
tag.textContent = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
var btn = document.querySelector('[data-check-pkg-status="' + pkgId + '"]');
|
||||||
|
if (btn) btn.remove();
|
||||||
|
|
||||||
|
var errEl = document.querySelector('[data-pkg-error="' + pkgId + '"]');
|
||||||
|
if (!errEl && tag) {
|
||||||
|
errEl = document.createElement('div');
|
||||||
|
errEl.className = 'muted mt-4';
|
||||||
|
errEl.style.fontSize = '0.75rem';
|
||||||
|
errEl.setAttribute('data-pkg-error', pkgId);
|
||||||
|
tag.parentNode.appendChild(errEl);
|
||||||
|
}
|
||||||
|
if (errEl) errEl.textContent = errorMsg || 'Blad tworzenia przesylki';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollPackageStatus(pkgId, orderId, btn, attempt) {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Sprawdzam... (' + (attempt + 1) + ')';
|
||||||
|
}
|
||||||
|
fetch('/orders/' + orderId + '/shipment/' + pkgId + '/status')
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (data.status === 'label_ready' || data.status === 'created') {
|
||||||
|
updateRowSuccess(pkgId, orderId, data);
|
||||||
|
} else if (data.status === 'in_progress' || data.status === 'pending') {
|
||||||
|
if (attempt < 12) {
|
||||||
|
var delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
||||||
|
setTimeout(function () {
|
||||||
|
pollPackageStatus(pkgId, orderId, btn, attempt + 1);
|
||||||
|
}, delay);
|
||||||
|
} else if (btn) {
|
||||||
|
btn.textContent = 'Sprawdz ponownie';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
updateRowError(pkgId, data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (btn) {
|
||||||
|
btn.textContent = 'Blad sieci';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-check-pkg-status]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
pollPackageStatus(
|
||||||
|
btn.getAttribute('data-check-pkg-status'),
|
||||||
|
btn.getAttribute('data-order-id'),
|
||||||
|
btn,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-check-pkg-status][data-auto-poll="1"]').forEach(function (btn, idx) {
|
||||||
|
setTimeout(function () {
|
||||||
|
pollPackageStatus(
|
||||||
|
btn.getAttribute('data-check-pkg-status'),
|
||||||
|
btn.getAttribute('data-order-id'),
|
||||||
|
btn,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}, 500 + idx * 400);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var btnAdd = document.getElementById('btn-add-payment');
|
||||||
|
var btnSave = document.getElementById('btn-save-payment');
|
||||||
|
var btnCancel = document.getElementById('btn-cancel-payment');
|
||||||
|
var form = document.getElementById('payment-add-form');
|
||||||
|
if (!btnAdd || !form) return;
|
||||||
|
|
||||||
|
btnAdd.addEventListener('click', function () {
|
||||||
|
form.style.display = '';
|
||||||
|
btnAdd.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCancel.addEventListener('click', function () {
|
||||||
|
form.style.display = 'none';
|
||||||
|
btnAdd.style.display = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
btnSave.addEventListener('click', function () {
|
||||||
|
var amount = parseFloat(document.getElementById('payment-amount').value);
|
||||||
|
var paymentType = document.getElementById('payment-type').value;
|
||||||
|
var paymentDate = document.getElementById('payment-date').value;
|
||||||
|
var comment = document.getElementById('payment-comment').value;
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
window.OrderProAlerts.error('Kwota musi być większa od 0.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!paymentType) {
|
||||||
|
window.OrderProAlerts.error('Wybierz typ płatności.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSave.disabled = true;
|
||||||
|
btnSave.textContent = 'Zapisywanie...';
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/orders/<?= (int) $orderId ?>/payment/add', true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||||
|
xhr.onload = function () {
|
||||||
|
var resp;
|
||||||
|
try { resp = JSON.parse(xhr.responseText); } catch (e) { resp = {}; }
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300 && resp.ok) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
window.OrderProAlerts.error(resp.error || 'Błąd zapisu płatności.');
|
||||||
|
btnSave.disabled = false;
|
||||||
|
btnSave.textContent = 'Zapisz';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
window.OrderProAlerts.error('Błąd połączenia z serwerem.');
|
||||||
|
btnSave.disabled = false;
|
||||||
|
btnSave.textContent = 'Zapisz';
|
||||||
|
};
|
||||||
|
xhr.send(
|
||||||
|
'_token=<?= $e($csrfToken) ?>' +
|
||||||
|
'&amount=' + encodeURIComponent(amount) +
|
||||||
|
'&payment_type_id=' + encodeURIComponent(paymentType) +
|
||||||
|
'&payment_date=' + encodeURIComponent(paymentDate) +
|
||||||
|
'&comment=' + encodeURIComponent(comment)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<?php require __DIR__ . '/partials/email-send-modal.php'; ?>
|
<?php require __DIR__ . '/partials/email-send-modal.php'; ?>
|
||||||
|
|||||||
@@ -90,42 +90,6 @@ $pastTotal = max(0, (int) ($pastPagination['total'] ?? 0));
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-16">
|
|
||||||
<h3 class="section-title"><?= $e($t('settings.cron.future_jobs_title')) ?></h3>
|
|
||||||
<div class="table-wrap mt-12">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th><?= $e($t('settings.cron.fields.job_type')) ?></th>
|
|
||||||
<th><?= $e($t('settings.cron.fields.status')) ?></th>
|
|
||||||
<th><?= $e($t('settings.cron.fields.priority')) ?></th>
|
|
||||||
<th><?= $e($t('settings.cron.fields.scheduled_at')) ?></th>
|
|
||||||
<th><?= $e($t('settings.cron.fields.attempts')) ?></th>
|
|
||||||
<th><?= $e($t('settings.cron.fields.last_error')) ?></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if ($futureJobsList === []): ?>
|
|
||||||
<tr><td class="muted" colspan="7"><?= $e($t('settings.cron.empty_future_jobs')) ?></td></tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($futureJobsList as $item): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= $e((string) ($item['id'] ?? 0)) ?></td>
|
|
||||||
<td><?= $e((string) ($item['job_type'] ?? '')) ?></td>
|
|
||||||
<td><?= $e((string) ($item['status'] ?? '')) ?></td>
|
|
||||||
<td><?= $e((string) ($item['priority'] ?? '')) ?></td>
|
|
||||||
<td><?= $e((string) ($item['scheduled_at'] ?? '')) ?></td>
|
|
||||||
<td><?= $e((string) ($item['attempts'] ?? 0) . '/' . (string) ($item['max_attempts'] ?? 0)) ?></td>
|
|
||||||
<td><?= $e((string) ($item['last_error'] ?? '')) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mt-16">
|
<section class="mt-16">
|
||||||
<h3 class="section-title"><?= $e($t('settings.cron.past_jobs_title')) ?></h3>
|
<h3 class="section-title"><?= $e($t('settings.cron.past_jobs_title')) ?></h3>
|
||||||
<div class="table-wrap mt-12">
|
<div class="table-wrap mt-12">
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ return static function (Application $app): void {
|
|||||||
$shipmentPackageRepositoryForOrders
|
$shipmentPackageRepositoryForOrders
|
||||||
);
|
);
|
||||||
$printJobRepository = new PrintJobRepository($app->db());
|
$printJobRepository = new PrintJobRepository($app->db());
|
||||||
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository);
|
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository, $shopproIntegrationsRepository);
|
||||||
$receiptController = new ReceiptController(
|
$receiptController = new ReceiptController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -483,6 +483,7 @@ return static function (Application $app): void {
|
|||||||
$router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]);
|
$router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/shipment/{packageId}/label', [$shipmentController, 'label'], [$authMiddleware]);
|
$router->post('/orders/{id}/shipment/{packageId}/label', [$shipmentController, 'label'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/shipment/manual', [$shipmentController, 'createManual'], [$authMiddleware]);
|
$router->post('/orders/{id}/shipment/manual', [$shipmentController, 'createManual'], [$authMiddleware]);
|
||||||
|
$router->post('/orders/{id}/payment/add', [$ordersController, 'addPayment'], [$authMiddleware]);
|
||||||
|
|
||||||
// --- Printing module ---
|
// --- Printing module ---
|
||||||
$printApiKeyRepository = new PrintApiKeyRepository($app->db());
|
$printApiKeyRepository = new PrintApiKeyRepository($app->db());
|
||||||
|
|||||||
@@ -157,8 +157,7 @@ final class CronHandlerFactory
|
|||||||
new ApaczkaIntegrationRepository($this->db, $this->integrationSecret)
|
new ApaczkaIntegrationRepository($this->db, $this->integrationSecret)
|
||||||
),
|
),
|
||||||
new AllegroTrackingService(
|
new AllegroTrackingService(
|
||||||
$apiClient,
|
new InpostIntegrationRepository($this->db, $this->integrationSecret)
|
||||||
$tokenManager
|
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
new ShipmentPackageRepository($this->db),
|
new ShipmentPackageRepository($this->db),
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ use App\Modules\Email\EmailSendingService;
|
|||||||
use App\Modules\Settings\EmailMailboxRepository;
|
use App\Modules\Settings\EmailMailboxRepository;
|
||||||
use App\Modules\Settings\EmailTemplateRepository;
|
use App\Modules\Settings\EmailTemplateRepository;
|
||||||
use App\Modules\Settings\ReceiptConfigRepository;
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
|
use App\Modules\Settings\ShopproApiClient;
|
||||||
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
use App\Modules\Shipments\ShipmentPackageRepository;
|
use App\Modules\Shipments\ShipmentPackageRepository;
|
||||||
|
|
||||||
final class OrdersController
|
final class OrdersController
|
||||||
@@ -32,7 +34,8 @@ final class OrdersController
|
|||||||
private readonly ?EmailTemplateRepository $emailTemplateRepo = null,
|
private readonly ?EmailTemplateRepository $emailTemplateRepo = null,
|
||||||
private readonly ?EmailMailboxRepository $emailMailboxRepo = null,
|
private readonly ?EmailMailboxRepository $emailMailboxRepo = null,
|
||||||
private readonly string $storagePath = '',
|
private readonly string $storagePath = '',
|
||||||
private readonly ?\App\Modules\Printing\PrintJobRepository $printJobRepo = null
|
private readonly ?\App\Modules\Printing\PrintJobRepository $printJobRepo = null,
|
||||||
|
private readonly ?ShopproIntegrationsRepository $shopproIntegrations = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,4 +784,102 @@ final class OrdersController
|
|||||||
return Response::json($preview);
|
return Response::json($preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addPayment(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return Response::json(['ok' => false, 'error' => 'Nieprawidłowe ID zamówienia.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Csrf::verify((string) $request->input('_token', ''))) {
|
||||||
|
return Response::json(['ok' => false, 'error' => 'Nieprawidłowy token CSRF.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = (float) $request->input('amount', 0);
|
||||||
|
$paymentTypeId = trim((string) $request->input('payment_type_id', ''));
|
||||||
|
$paymentDate = trim((string) $request->input('payment_date', ''));
|
||||||
|
$comment = trim((string) $request->input('comment', ''));
|
||||||
|
|
||||||
|
if ($amount <= 0) {
|
||||||
|
return Response::json(['ok' => false, 'error' => 'Kwota musi być większa od 0.'], 422);
|
||||||
|
}
|
||||||
|
if ($paymentTypeId === '') {
|
||||||
|
return Response::json(['ok' => false, 'error' => 'Wybierz typ płatności.'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->orders->addPayment($orderId, [
|
||||||
|
'amount' => $amount,
|
||||||
|
'payment_type_id' => $paymentTypeId,
|
||||||
|
'payment_date' => $paymentDate !== '' ? $paymentDate . ' ' . date('H:i:s') : '',
|
||||||
|
'comment' => $comment,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result === null) {
|
||||||
|
return Response::json(['ok' => false, 'error' => 'Nie udało się zapisać płatności.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'payment',
|
||||||
|
'Dodano płatność: ' . number_format($amount, 2, '.', ' ') . ' PLN (' . $paymentTypeId . ')',
|
||||||
|
['payment_id' => $result['id'], 'amount' => $amount, 'type' => $paymentTypeId],
|
||||||
|
'user',
|
||||||
|
$this->auth->user()['name'] ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->pushPaymentToShoppro($orderId, $result['payment_status']);
|
||||||
|
|
||||||
|
return Response::json([
|
||||||
|
'ok' => true,
|
||||||
|
'payment_id' => $result['id'],
|
||||||
|
'payment_status' => $result['payment_status'],
|
||||||
|
'total_paid' => $result['total_paid'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pushPaymentToShoppro(int $orderId, int $paymentStatus): void
|
||||||
|
{
|
||||||
|
if ($paymentStatus !== 2 || $this->shopproIntegrations === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$orderStmt = $this->orders->findOrderSourceInfo($orderId);
|
||||||
|
if ($orderStmt === null || ($orderStmt['source'] ?? '') !== 'shoppro') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$integrationId = (int) ($orderStmt['integration_id'] ?? 0);
|
||||||
|
$sourceOrderId = trim((string) ($orderStmt['source_order_id'] ?? ''));
|
||||||
|
if ($integrationId <= 0 || $sourceOrderId === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$integration = $this->shopproIntegrations->findIntegration($integrationId);
|
||||||
|
if ($integration === null || empty($integration['is_active']) || empty($integration['has_api_key'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = trim((string) ($integration['base_url'] ?? ''));
|
||||||
|
$apiKey = $this->shopproIntegrations->getApiKeyDecrypted($integrationId);
|
||||||
|
if ($baseUrl === '' || $apiKey === null || trim($apiKey) === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = new ShopproApiClient();
|
||||||
|
$pushResult = $client->setOrderPaid($baseUrl, $apiKey, 10, $sourceOrderId);
|
||||||
|
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'sync',
|
||||||
|
$pushResult['ok']
|
||||||
|
? 'Wysłano status płatności do shopPRO (opłacone)'
|
||||||
|
: 'Błąd push płatności do shopPRO: ' . ($pushResult['message'] ?? 'unknown'),
|
||||||
|
['direction' => 'push', 'target' => 'shoppro', 'ok' => $pushResult['ok']],
|
||||||
|
'system'
|
||||||
|
);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -847,6 +847,94 @@ final class OrdersRepository
|
|||||||
], $actorType, $actorName);
|
], $actorType, $actorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data Keys: payment_type_id, amount, payment_date, comment, currency
|
||||||
|
* @return array{id:int, payment_status:int, total_paid:float}|null
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @return array{source:string, integration_id:int, source_order_id:string}|null
|
||||||
|
*/
|
||||||
|
public function findOrderSourceInfo(int $orderId): ?array
|
||||||
|
{
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$stmt = $this->pdo->prepare('SELECT source, integration_id, source_order_id FROM orders WHERE id = :id LIMIT 1');
|
||||||
|
$stmt->execute(['id' => $orderId]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data Keys: payment_type_id, amount, payment_date, comment, currency
|
||||||
|
* @return array{id:int, payment_status:int, total_paid:float}|null
|
||||||
|
*/
|
||||||
|
public function addPayment(int $orderId, array $data): ?array
|
||||||
|
{
|
||||||
|
if ($orderId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->pdo->prepare('SELECT id, total_with_tax, currency FROM orders WHERE id = :id LIMIT 1');
|
||||||
|
$stmt->execute(['id' => $orderId]);
|
||||||
|
$order = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!is_array($order)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = round((float) ($data['amount'] ?? 0), 2);
|
||||||
|
$paymentTypeId = trim((string) ($data['payment_type_id'] ?? ''));
|
||||||
|
$paymentDate = trim((string) ($data['payment_date'] ?? ''));
|
||||||
|
$comment = trim((string) ($data['comment'] ?? ''));
|
||||||
|
$currency = trim((string) ($data['currency'] ?? $order['currency'] ?? 'PLN'));
|
||||||
|
|
||||||
|
if ($amount <= 0 || $paymentTypeId === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourcePaymentId = 'manual_' . $orderId . '_' . time();
|
||||||
|
|
||||||
|
$insert = $this->pdo->prepare(
|
||||||
|
'INSERT INTO order_payments (order_id, source_payment_id, payment_type_id, payment_date, amount, currency, comment, created_at, updated_at)
|
||||||
|
VALUES (:order_id, :source_payment_id, :payment_type_id, :payment_date, :amount, :currency, :comment, NOW(), NOW())'
|
||||||
|
);
|
||||||
|
$insert->execute([
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'source_payment_id' => $sourcePaymentId,
|
||||||
|
'payment_type_id' => $paymentTypeId,
|
||||||
|
'payment_date' => $paymentDate !== '' ? $paymentDate : date('Y-m-d H:i:s'),
|
||||||
|
'amount' => $amount,
|
||||||
|
'currency' => $currency,
|
||||||
|
'comment' => $comment !== '' ? $comment : null,
|
||||||
|
]);
|
||||||
|
$paymentId = (int) $this->pdo->lastInsertId();
|
||||||
|
|
||||||
|
$sumStmt = $this->pdo->prepare('SELECT COALESCE(SUM(amount), 0) FROM order_payments WHERE order_id = :order_id');
|
||||||
|
$sumStmt->execute(['order_id' => $orderId]);
|
||||||
|
$totalPaid = round((float) $sumStmt->fetchColumn(), 2);
|
||||||
|
|
||||||
|
$totalWithTax = $order['total_with_tax'] !== null ? (float) $order['total_with_tax'] : null;
|
||||||
|
$paymentStatus = 0;
|
||||||
|
if ($totalPaid > 0 && $totalWithTax !== null && $totalPaid >= $totalWithTax) {
|
||||||
|
$paymentStatus = 2;
|
||||||
|
} elseif ($totalPaid > 0) {
|
||||||
|
$paymentStatus = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = $this->pdo->prepare('UPDATE orders SET payment_status = :payment_status, total_paid = :total_paid, updated_at = NOW() WHERE id = :id');
|
||||||
|
$update->execute([
|
||||||
|
'payment_status' => $paymentStatus,
|
||||||
|
'total_paid' => $totalPaid,
|
||||||
|
'id' => $orderId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $paymentId,
|
||||||
|
'payment_status' => $paymentStatus,
|
||||||
|
'total_paid' => $totalPaid,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool
|
public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ final class AllegroIntegrationController
|
|||||||
];
|
];
|
||||||
private const OAUTH_SCOPES = [
|
private const OAUTH_SCOPES = [
|
||||||
AllegroOAuthClient::ORDERS_READ_SCOPE,
|
AllegroOAuthClient::ORDERS_READ_SCOPE,
|
||||||
|
AllegroOAuthClient::ORDERS_WRITE_SCOPE,
|
||||||
AllegroOAuthClient::SALE_OFFERS_READ_SCOPE,
|
AllegroOAuthClient::SALE_OFFERS_READ_SCOPE,
|
||||||
AllegroOAuthClient::SHIPMENTS_READ_SCOPE,
|
AllegroOAuthClient::SHIPMENTS_READ_SCOPE,
|
||||||
AllegroOAuthClient::SHIPMENTS_WRITE_SCOPE,
|
AllegroOAuthClient::SHIPMENTS_WRITE_SCOPE,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Core\Exceptions\AllegroOAuthException;
|
|||||||
final class AllegroOAuthClient
|
final class AllegroOAuthClient
|
||||||
{
|
{
|
||||||
public const ORDERS_READ_SCOPE = 'allegro:api:orders:read';
|
public const ORDERS_READ_SCOPE = 'allegro:api:orders:read';
|
||||||
|
public const ORDERS_WRITE_SCOPE = 'allegro:api:orders:write';
|
||||||
public const SALE_OFFERS_READ_SCOPE = 'allegro:api:sale:offers:read';
|
public const SALE_OFFERS_READ_SCOPE = 'allegro:api:sale:offers:read';
|
||||||
public const SHIPMENTS_READ_SCOPE = 'allegro:api:shipments:read';
|
public const SHIPMENTS_READ_SCOPE = 'allegro:api:shipments:read';
|
||||||
public const SHIPMENTS_WRITE_SCOPE = 'allegro:api:shipments:write';
|
public const SHIPMENTS_WRITE_SCOPE = 'allegro:api:shipments:write';
|
||||||
|
|||||||
@@ -249,6 +249,34 @@ final class ShopproApiClient
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{ok:bool,http_code:int|null,message:string}
|
||||||
|
*/
|
||||||
|
public function setOrderPaid(
|
||||||
|
string $baseUrl,
|
||||||
|
string $apiKey,
|
||||||
|
int $timeoutSeconds,
|
||||||
|
string $sourceOrderId
|
||||||
|
): array {
|
||||||
|
if ($sourceOrderId === '') {
|
||||||
|
return ['ok' => false, 'http_code' => null, 'message' => 'Brak source_order_id.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim(trim($baseUrl), '/') . '/api.php?' . http_build_query([
|
||||||
|
'endpoint' => 'orders',
|
||||||
|
'action' => 'set_paid',
|
||||||
|
'id' => $sourceOrderId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->requestJsonPut($url, $apiKey, $timeoutSeconds, json_encode(['send_email' => 0], JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => ($response['ok'] ?? false) === true,
|
||||||
|
'http_code' => $response['http_code'] ?? null,
|
||||||
|
'message' => (string) ($response['message'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{ok:bool,http_code:int|null,message:string,data:array<string,mixed>|array<int,mixed>|null}
|
* @return array{ok:bool,http_code:int|null,message:string,data:array<string,mixed>|array<int,mixed>|null}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ final class AllegroShipmentService implements ShipmentProviderInterface
|
|||||||
'deliveryMethodId' => $deliveryMethodId,
|
'deliveryMethodId' => $deliveryMethodId,
|
||||||
'sender' => $senderAddress,
|
'sender' => $senderAddress,
|
||||||
'receiver' => $receiverAddress,
|
'receiver' => $receiverAddress,
|
||||||
'referenceNumber' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
'referenceNumber' => substr($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId, 0, 35),
|
||||||
'packages' => [[
|
'packages' => [[
|
||||||
'type' => $packageType,
|
'type' => $packageType,
|
||||||
'length' => ['value' => $lengthCm, 'unit' => 'CENTIMETER'],
|
'length' => ['value' => $lengthCm, 'unit' => 'CENTIMETER'],
|
||||||
@@ -140,7 +140,7 @@ final class AllegroShipmentService implements ShipmentProviderInterface
|
|||||||
'label_format' => $labelFormat,
|
'label_format' => $labelFormat,
|
||||||
'receiver_point_id' => trim((string) ($formData['receiver_point_id'] ?? '')),
|
'receiver_point_id' => trim((string) ($formData['receiver_point_id'] ?? '')),
|
||||||
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
||||||
'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
'reference_number' => substr($sourceOrderId !== '' ? $sourceOrderId : (string) $orderId, 0, 35),
|
||||||
'payload_json' => $apiPayload,
|
'payload_json' => $apiPayload,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -196,10 +196,11 @@ final class AllegroShipmentService implements ShipmentProviderInterface
|
|||||||
|
|
||||||
if ($status === 'SUCCESS' && $shipmentId !== '') {
|
if ($status === 'SUCCESS' && $shipmentId !== '') {
|
||||||
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
||||||
$trackingNumber = trim((string) ($details['waybill'] ?? ''));
|
$detailPackages = is_array($details['packages'] ?? null) ? $details['packages'] : [];
|
||||||
|
$trackingNumber = trim((string) ($detailPackages[0]['waybill'] ?? ''));
|
||||||
$carrierId = trim((string) ($package['carrier_id'] ?? ''));
|
$carrierId = trim((string) ($package['carrier_id'] ?? ''));
|
||||||
if ($carrierId === '') {
|
if ($carrierId === '') {
|
||||||
$carrierId = trim((string) ($details['carrierId'] ?? ''));
|
$carrierId = trim((string) ($details['carrier'] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->packages->update($packageId, [
|
$this->packages->update($packageId, [
|
||||||
@@ -264,7 +265,7 @@ final class AllegroShipmentService implements ShipmentProviderInterface
|
|||||||
|
|
||||||
[$accessToken, $env] = $this->tokenManager->resolveToken();
|
[$accessToken, $env] = $this->tokenManager->resolveToken();
|
||||||
$labelFormat = trim((string) ($package['label_format'] ?? 'PDF'));
|
$labelFormat = trim((string) ($package['label_format'] ?? 'PDF'));
|
||||||
$pageSize = $labelFormat === 'ZPL' ? 'A6' : 'A4';
|
$pageSize = 'A6';
|
||||||
$binary = $this->apiClient->getShipmentLabel($env, $accessToken, [$shipmentId], $pageSize);
|
$binary = $this->apiClient->getShipmentLabel($env, $accessToken, [$shipmentId], $pageSize);
|
||||||
|
|
||||||
$dir = rtrim($storagePath, '/\\') . '/labels';
|
$dir = rtrim($storagePath, '/\\') . '/labels';
|
||||||
@@ -286,10 +287,11 @@ final class AllegroShipmentService implements ShipmentProviderInterface
|
|||||||
if (trim((string) ($package['tracking_number'] ?? '')) === '') {
|
if (trim((string) ($package['tracking_number'] ?? '')) === '') {
|
||||||
try {
|
try {
|
||||||
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
||||||
$trackingNumber = trim((string) ($details['waybill'] ?? ''));
|
$detailPackages = is_array($details['packages'] ?? null) ? $details['packages'] : [];
|
||||||
|
$trackingNumber = trim((string) ($detailPackages[0]['waybill'] ?? ''));
|
||||||
$carrierId = trim((string) ($package['carrier_id'] ?? ''));
|
$carrierId = trim((string) ($package['carrier_id'] ?? ''));
|
||||||
if ($carrierId === '') {
|
if ($carrierId === '') {
|
||||||
$carrierId = trim((string) ($details['carrierId'] ?? ''));
|
$carrierId = trim((string) ($details['carrier'] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($trackingNumber !== '') {
|
if ($trackingNumber !== '') {
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Modules\Shipments;
|
namespace App\Modules\Shipments;
|
||||||
|
|
||||||
use App\Modules\Settings\AllegroApiClient;
|
use App\Modules\Settings\InpostIntegrationRepository;
|
||||||
use App\Modules\Settings\AllegroTokenManager;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class AllegroTrackingService implements ShipmentTrackingInterface
|
final class AllegroTrackingService implements ShipmentTrackingInterface
|
||||||
{
|
{
|
||||||
|
private const INPOST_API_PRODUCTION = 'https://api-shipx-pl.easypack24.net/v1';
|
||||||
|
private const INPOST_API_SANDBOX = 'https://sandbox-api-shipx-pl.easypack24.net/v1';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AllegroApiClient $apiClient,
|
private readonly InpostIntegrationRepository $inpostRepository
|
||||||
private readonly AllegroTokenManager $tokenManager
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,32 +23,122 @@ final class AllegroTrackingService implements ShipmentTrackingInterface
|
|||||||
|
|
||||||
public function getDeliveryStatus(array $package): ?array
|
public function getDeliveryStatus(array $package): ?array
|
||||||
{
|
{
|
||||||
$shipmentId = trim((string) ($package['shipment_id'] ?? ''));
|
$trackingNumber = trim((string) ($package['tracking_number'] ?? ''));
|
||||||
if ($shipmentId === '') {
|
if ($trackingNumber === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->fetchStatus($shipmentId);
|
$carrierId = strtolower(trim((string) ($package['carrier_id'] ?? '')));
|
||||||
|
|
||||||
|
if (str_contains($carrierId, 'inpost') || str_contains($carrierId, 'paczkomat')) {
|
||||||
|
return $this->fetchInpostStatus($trackingNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allegro Delivery (One Kurier), DHL, DPD via Allegro — brak publicznego API trackingu
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchStatus(string $shipmentId): ?array
|
private function fetchInpostStatus(string $trackingNumber): ?array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
[$accessToken, $env] = $this->tokenManager->resolveToken();
|
$token = $this->resolveInpostToken();
|
||||||
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
if ($token === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$rawStatus = strtoupper(trim((string) ($details['status'] ?? '')));
|
$settings = $this->inpostRepository->getSettings();
|
||||||
|
$env = (string) ($settings['environment'] ?? 'sandbox');
|
||||||
|
$baseUrl = strtolower(trim($env)) === 'production'
|
||||||
|
? self::INPOST_API_PRODUCTION
|
||||||
|
: self::INPOST_API_SANDBOX;
|
||||||
|
|
||||||
|
$url = $baseUrl . '/tracking/' . rawurlencode($trackingNumber);
|
||||||
|
$response = $this->apiRequest($url, $token);
|
||||||
|
|
||||||
|
$details = is_array($response['tracking_details'] ?? null) ? $response['tracking_details'] : [];
|
||||||
|
if ($details === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawStatus = strtolower(trim((string) ($details[0]['status'] ?? '')));
|
||||||
if ($rawStatus === '') {
|
if ($rawStatus === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => DeliveryStatus::normalize('allegro_wza', $rawStatus),
|
'status' => DeliveryStatus::normalize('inpost', $rawStatus),
|
||||||
'status_raw' => $rawStatus,
|
'status_raw' => $rawStatus,
|
||||||
'description' => DeliveryStatus::description('allegro_wza', $rawStatus),
|
'description' => DeliveryStatus::description('inpost', $rawStatus),
|
||||||
];
|
];
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveInpostToken(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$token = $this->inpostRepository->getDecryptedToken();
|
||||||
|
return ($token !== null && trim($token) !== '') ? trim($token) : null;
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function apiRequest(string $url, string $token): array
|
||||||
|
{
|
||||||
|
$ch = curl_init($url);
|
||||||
|
if ($ch === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$opts = [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 15,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => 2,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . $token,
|
||||||
|
'Accept: application/json',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$caPath = $this->getCaBundlePath();
|
||||||
|
if ($caPath !== null) {
|
||||||
|
$opts[CURLOPT_CAINFO] = $caPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_setopt_array($ch, $opts);
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$ch = null;
|
||||||
|
|
||||||
|
if ($body === false || $httpCode < 200 || $httpCode >= 300) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_decode((string) $body, true);
|
||||||
|
return is_array($json) ? $json : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCaBundlePath(): ?string
|
||||||
|
{
|
||||||
|
$candidates = [
|
||||||
|
(string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? ''),
|
||||||
|
(string) ini_get('curl.cainfo'),
|
||||||
|
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||||
|
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||||
|
'/etc/ssl/certs/ca-certificates.crt',
|
||||||
|
];
|
||||||
|
foreach ($candidates as $path) {
|
||||||
|
if ($path !== '' && is_file($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,10 +308,6 @@ final class DeliveryStatus
|
|||||||
return 'https://inpost.pl/sledzenie-przesylek?number=' . $encoded;
|
return 'https://inpost.pl/sledzenie-przesylek?number=' . $encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($provider === 'allegro_wza') {
|
|
||||||
return 'https://allegro.pl/przesylka/' . $encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($carrierId !== '') {
|
if ($carrierId !== '') {
|
||||||
$url = self::matchCarrierByName($encoded, strtolower(trim($carrierId)));
|
$url = self::matchCarrierByName($encoded, strtolower(trim($carrierId)));
|
||||||
if ($url !== null) {
|
if ($url !== null) {
|
||||||
@@ -319,6 +315,10 @@ final class DeliveryStatus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($provider === 'allegro_wza') {
|
||||||
|
return 'https://allegro.pl/allegrodelivery/sledzenie-paczki?numer=' . $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki';
|
return 'https://www.google.com/search?q=' . $encoded . '+sledzenie+przesylki';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +348,9 @@ final class DeliveryStatus
|
|||||||
if (str_contains($carrier, 'gls')) {
|
if (str_contains($carrier, 'gls')) {
|
||||||
return 'https://gls-group.com/PL/pl/sledzenie-paczek?match=' . $encoded;
|
return 'https://gls-group.com/PL/pl/sledzenie-paczek?match=' . $encoded;
|
||||||
}
|
}
|
||||||
|
if ($carrier === 'allegro') {
|
||||||
|
return 'https://allegro.pl/allegrodelivery/sledzenie-paczki?numer=' . $encoded;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user