Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332bf92e44 | ||
|
|
cceeb83d5e | ||
|
|
1a7e8704d0 |
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [0.27.2] - 2026-09-29
|
||||||
|
### Fixed
|
||||||
|
- `Modules\Core\Order\Services\TransactionRecorder::record()` — made idempotent
|
||||||
|
on `(order_id, type, reference)`. A successful Stripe payment can legitimately
|
||||||
|
report `PaymentCaptured` twice for the same PaymentIntent (checkout's
|
||||||
|
synchronous capture via `pay()`, then the webhook confirming the same
|
||||||
|
outcome asynchronously via `handleCallback()`), both routing through
|
||||||
|
`resultFromIntent()`. With no dedupe check, this wrote two identical
|
||||||
|
`Transaction` rows for one real payment. Now returns the existing row
|
||||||
|
instead of creating a duplicate.
|
||||||
|
|
||||||
|
## [0.27.1] - 2026-09-29
|
||||||
|
### Added
|
||||||
|
- Temp logger for Stripe webhook
|
||||||
|
|
||||||
## [0.27.0] - 2026-09-29
|
## [0.27.0] - 2026-09-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+2
-4
@@ -2,7 +2,7 @@
|
|||||||
"name": "boboko/core",
|
"name": "boboko/core",
|
||||||
"description": "Core module — authentication and shared panel behaviour",
|
"description": "Core module — authentication and shared panel behaviour",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "0.27.0",
|
"version": "0.27.2",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
@@ -18,9 +18,7 @@
|
|||||||
"lunarphp/search": "*",
|
"lunarphp/search": "*",
|
||||||
"lunarphp/meilisearch": "*",
|
"lunarphp/meilisearch": "*",
|
||||||
"spatie/laravel-translation-loader": "^2.8",
|
"spatie/laravel-translation-loader": "^2.8",
|
||||||
"stripe/stripe-php": "^16.6",
|
"stripe/stripe-php": "^16.6"
|
||||||
"picqer/php-barcode-generator": "^3.3",
|
|
||||||
"barryvdh/laravel-dompdf": "^3.1"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| ELTA Courier credentials
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| ELTA's API is SOAP (unlike ACS's JSON gateway or Box Now's REST +
|
|
||||||
| OAuth2). Two operation families live at the same endpoint — see
|
|
||||||
| Modules\Core\Shipping\Carriers\Elta\EltaClient's docblock for why only
|
|
||||||
| the "*NEW" family (create/track/station/cancel) is used; the old
|
|
||||||
| family (including its label-print operation) is unreachable with this
|
|
||||||
| account and isn't called anywhere in this integration. Labels are
|
|
||||||
| rendered locally instead — see
|
|
||||||
| Modules\Core\Shipping\Carriers\Elta\EltaLabelRenderer — which is why
|
|
||||||
| sender identity (name/address, unlike PELVGNEW's request which needs
|
|
||||||
| only apost_code) is configured here.
|
|
||||||
|
|
|
||||||
| Set these via environment variables — never commit real values.
|
|
||||||
|
|
|
||||||
| ELTA_SERVICE_LOCATION SOAP endpoint (defaults to the live production
|
|
||||||
| host; only needed if ELTA gives you a separate
|
|
||||||
| sandbox host).
|
|
||||||
| ELTA_USER_CODE Account user code (pel_user_code / pel_user).
|
|
||||||
| ELTA_USER_PASS Account security code — not used by anything the
|
|
||||||
| *NEW family calls; kept only in case ELTA ever
|
|
||||||
| asks for it.
|
|
||||||
| ELTA_APOST_CODE Sender/account code (pel_apost_code).
|
|
||||||
| ELTA_APOST_SUB_CODE Sender sub-code (pel_apost_sub_code).
|
|
||||||
| ELTA_SENDER_NAME Sender name printed on the label — PELVGNEW's
|
|
||||||
| own request has no such field, so this only
|
|
||||||
| matters for our own locally-rendered label.
|
|
||||||
| ELTA_SENDER_ADDRESS Sender street address printed on the label.
|
|
||||||
| ELTA_SENDER_POSTCODE Sender postcode printed on the label.
|
|
||||||
| ELTA_SENDER_AREA Sender area/city printed on the label.
|
|
||||||
| ELTA_SENDER_PHONE Sender phone printed on the label.
|
|
||||||
| ELTA_ORIGIN_STATION_CODE This account's home ELTA station code — shown
|
|
||||||
| on the label as "Γ.Κατάθεσης"/"Από". ELTA's own
|
|
||||||
| client gets this from a PELLOGINNEW response
|
|
||||||
| (user_station); configured here instead so a
|
|
||||||
| label print doesn't need an extra API call.
|
|
||||||
| ELTA_LABEL_PAPER_SIZE "a4" (default, 3 copies per page) or "a6"
|
|
||||||
| (single thermal label) — matches the real
|
|
||||||
| client's own one-time printer-setup choice, not
|
|
||||||
| varied per shipment.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'service_location' => env('ELTA_SERVICE_LOCATION', 'http://212.205.47.226:9003'),
|
|
||||||
|
|
||||||
'user_code' => env('ELTA_USER_CODE'),
|
|
||||||
'user_pass' => env('ELTA_USER_PASS'),
|
|
||||||
|
|
||||||
'apost_code' => env('ELTA_APOST_CODE'),
|
|
||||||
'apost_sub_code' => env('ELTA_APOST_SUB_CODE'),
|
|
||||||
|
|
||||||
'sender_name' => env('ELTA_SENDER_NAME'),
|
|
||||||
'sender_address' => env('ELTA_SENDER_ADDRESS'),
|
|
||||||
'sender_postcode' => env('ELTA_SENDER_POSTCODE'),
|
|
||||||
'sender_area' => env('ELTA_SENDER_AREA'),
|
|
||||||
'sender_phone' => env('ELTA_SENDER_PHONE'),
|
|
||||||
'origin_station_code' => env('ELTA_ORIGIN_STATION_CODE'),
|
|
||||||
|
|
||||||
'label_paper_size' => env('ELTA_LABEL_PAPER_SIZE', 'a4'),
|
|
||||||
|
|
||||||
'timeout' => env('ELTA_HTTP_TIMEOUT', 15),
|
|
||||||
|
|
||||||
];
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@boboko/core",
|
"name": "@boboko/core",
|
||||||
"version": "0.27.0",
|
"version": "0.27.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.",
|
"description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="151" height="150" viewBox="0 0 151 150"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><image width="151" height="150" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJcAAACWCAYAAADTwxrcAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAOdEVYdFNvZnR3YXJlAEZpZ21hnrGWYwAACYRJREFUeAHtnctuG0cWhv8qJuuRgWSAWU1nMrO28gRuP4FkzANIegJZyAXIStQqQOLA8hOY2ieR/QSmnyDMOgnSuwCJAzLrmF05p6spURSvYpNddfp8gO6UxGb9fc6p21+AoiiKoiiKoiiKoiiKoiiKoiiKoiiKoiiKRAyUxVz2d+j9TvH5o3sZlKVQcY1z2d9FjhQW9+GwS99JMBLVbbLizaCHIV6jhS4JbwDlChXXZT+l93skpkPMFtKydOnvXOD/9zpQGiwuFpXDKX2Wono4onXo40WT02jzxLVZUU3iRfbo3hkaSHPE5YvyUxLWY2wfFtnDpkUxiybAhbrD9zUJi0nof/9Cz+MUDUK+uF72D6hhX8H3/OrFoU0Ce4qGIDstcqTgBg2PLr3yj6QPXcgVV7jCGtEr6zCxApOZFr/p7wcuLIbrwEsIRp64LvsJXdVzxEEquQaTlRZ5uIF7hSEU76vg02MXwpAWubirnyA2HEVaPw4nCjni8mNZdY1jrUsCf2OIQo64Yi+O+cbwk+hikCGub/uHiDEdTuJkRS8Z4jJiGiWVFL3iF5dvjARScNiHECRErgPI4kBKzzF+cQm600t2kMu4prjF5VOiuPEhWPsAAohbXPlWVpNuH5dr5KodAxF3+BR2ijnSyIm95pKXEq/ZReTELq7oG2Amefw3TrziEjjRO0GCyIk5ckkXV/Q0Y/ePUgsqLmVjqLiUjRGvuOTvXo5+V1DskUvuvj9bWDRFTeziyiAXjVw18wPk0kPkxC0uhy5k0pOwEztucVmx4noNAcQtLt9j7EIaBi8gAAkrUUXc5WNkUnZfxy8ui3NIGpIwEGNxGb+4fOF7ARlkEJTmpexblBG9nCz3Zxni4gZxeIa4ycoULwY5E9e+YTLECtdawlwG5YiLG8bgCDFiLHvVdyAMWUtufBc+tvSYAbnIQxBkGu5+12dr8BThw9H2I6nLh2QuFmQb7hjqL07jgtelyRSXr78eImSBeWGJmOaZhdxlzhwRwhTYoBRWB8KRvYZ+JLBwJoJHB0x10ACac2rZt/12rQ6ExpDA3VGTTpNt1nmLbO7hjXm3aQPAafCkKdFqnGaeFHvZPyzNbRNsjkExJcUzBw09+7rZZ1x7kbHtZYrqaLyoRugB6oz3wuLDqPZwN6ENyk4DL/0Rsf69ClRck3j3nN3ijW2MLP596zEOf9Irl9HPB8U6/gYfkq4oiqIoiqIoiqIoihIIWxnn+hVfJNO+/y98nkHZCO6XwpB4minxwHywnW14lYqrj6c7b/GWBiDtnoFLDMyuo4+Y/wR6Dsjosa+HGHb/ic+itw7aNoWQhjTD4E8U4QHgBPPdrnlGoUeDwb3CDqGFHgkuQ8VUIq7f8VVK4jigt30S01oW3vQ3MhJbtwV7dg8nGZSpFILKcUgv2B4JJMW6GJppyHFh/osOKmItcb3B14d0mVVP/I5hOiqym5Tp7piE8Bib8eLnzbkd+nixbjS7k7jKSMVLVlJsBRXZFkQ1Ce9iP1snkq0kLq6pcuSnlPoeY8uU6fLsPXzcQcMgYaUkqueo48gWTpcGR3eJYkuLi4S1S8K6XFSgbxoHc/4+Pj5BQyBhcbSq20OCo9gJRbGV9iIsJa43eMLF+vm6xXpVcBSzsA8lp8myYOdotY9QsGhTBFt6d/hCcf2BrzkNthEYkgVWCot3jW9zrf9yrCCwueL6A0+Oqc4J1tZHqsDcz/geYZ8l2TEfLjZ9mblvkVLhfsjCYrj+ozrwFdWDCYRAUespwj+k9JCe58JtelPFxY1FUeEpIoAFRiP7zyEAilin5VBD+ORok8AO5j3ETv+9/FXdvcIVSWnsLY5GmUEx3AC0ERPUi6Xnncz68S1xlQX8zF8IFY60v+HL0NPJVIoG8j3D2Nih1DHzed8QF6fDEHuGy0LFfRSpfArHqGOAtApoXpPS+dSscUNcAmqX1M93xkMZtaJO6cRpOT11gytx8Xwh4nDjmwuNrdRnNnIX8sie73R2pt0gV+IqJ6Kjh+vFWKJXWQwfQgbHk9GrEFc5TpRCDO4AMSAjao3w68vGKMQ1RC7pIpm0TPOhk0ISpvDauML67wm7SE+KgHE/Fs8vgSS45ziWGi0vpYlxXGsRFmYPIWMDWu1QJWOp0VJKjHLgcRE0L7rLixsRLg8gEYv715/CiRQX43ciBYvM1z2/Lke45roPuQTZgFSXiL2hMVZHWhrfSiCUYK/tLUJO12szmsy2Eov5Mf6BEDHCeom3KW4e2YccKHWh4lI2i4pL2RgqLmVjcG8xg1z+RJhsxcKoRjJ+x4OoYi+UesIZQqQV8UHvy1FoiqZ/8AOEEnBUziCXK3M5SouuB6HkyDMESPniZ5AIm8qVcEEvUlwctYJ2KWRHP4mMXZd9H592IbDAZHdChE0XErHX11UMReTAS4jDhX1NrWCORq6SjFJ+d/RFuRLVdSAITonv4ZOgG6+ou5y46NUd/6IQl7TUGEFK9Ljlva6iwN68nqsRehoTegYhsH8qIsD8j24COdGrM2lteSWud/DOuYzRetOJyq9LSvSyt6/jSlzUIAM2tEXE8M0RS9QaISR6daYZ8t6YuC6dkruIFL45onQZbCFmA+FsWtRibq2KaKHFdoQRFvemE6uNON31PNgbp8DYq36Gjfgtcfk730R1oTGmw0nMhziPMD3OPQRhpuHuGzxhP9RjBA4Ja2BhP5JgulvaKbGLc4LQMeia/+DhvIfMXCxIg5BsiXOBwBlieCLFzblIL7ZosNDLkoxP1Vj0oLkrUan+emwCntjOkR/R5HQHgohAYMXzW+a4lrni4uEJixZfaFBTKZwKJQprRFHge4FlCIvessJilj7753d81Q7BIK482OARCV/sOrQRQdVgDs8olbVXOWV2pVPL2POKGvZ5fRtpzQvqFR5xREWDcD+iTRGjrht7QAI/o8He8xV/bzVxMexCSEU0F/tb60n64/DcSegrHTZJLVFsjePw/K/fERbZXxi2qWjbmEVkWVs943nPpkWrWbifiqOHOYol2BQsqmERrbpYgzuLa8QmRKaiWkwhMn7NTYUOihWJ6vrPVQQbrQ2R7+dwaQvmwap1Gae+IdxLA/eiXF+mLEGZLvep4N67g9AGxYYKh5d8rvUqxfoyVCauSVhsbL5mYBOHPMHUf26zHMPeu3g30whVDYX315AE5510plk18QrYAfX8uuaD4IY6FEVRFEVRFEVRFEVRFEVRFEVRFEVRFEVRFKXJ/A2oC/VTZ0o/1AAAAABJRU5ErkJggg=="></image></svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,123 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="el">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
{{--
|
|
||||||
Geometry is transcribed directly from ELTA_PEL.sydetaE.rdlc
|
|
||||||
(the "delivery copy" RDLC ELTA's own client selects for
|
|
||||||
printer_size==1, i.e. plain A4, per Sydeta.cs::print_vg()) —
|
|
||||||
every .mc-*/.ps-* absolute position below is that report's
|
|
||||||
own Top/Left in cm, walked through its Rectangle/Textbox
|
|
||||||
nesting so each value is already relative to its own
|
|
||||||
containing band. Static label text and font sizes/weights
|
|
||||||
also come from the RDLC's own Textbox/Style elements, not
|
|
||||||
guessed from the screenshot in ELTA's setup manual (that
|
|
||||||
manual image was used only as a sanity check afterwards).
|
|
||||||
--}}
|
|
||||||
@page { margin: 0; size: 21cm 29.7cm; }
|
|
||||||
html, body { margin: 0; padding: 0; }
|
|
||||||
body { font-family: 'DejaVu Sans', sans-serif; font-size: 6.5pt; color: #000; }
|
|
||||||
|
|
||||||
.page { position: relative; width: 21cm; height: 29.7cm; }
|
|
||||||
|
|
||||||
/* Each RDLC "band" (one ΣΥΝΟΔΕΥΤΙΚΟ ΔΕΛΤΙΟ copy) is its own
|
|
||||||
positioning context, so every child offset below is a direct
|
|
||||||
transcription of the RDLC's own relative-to-band Top/Left —
|
|
||||||
no manual offset arithmetic. */
|
|
||||||
.band { position: absolute; left: 0.27cm; }
|
|
||||||
.band > * { position: absolute; }
|
|
||||||
|
|
||||||
.mc-logo, .mc-company, .mc-box, .mc-cell, .mc-partybox { position: absolute; }
|
|
||||||
.mc-logo img { width: 100%; height: 100%; object-fit: contain; }
|
|
||||||
.mc-company { font-size: 4.6pt; line-height: 1.25; }
|
|
||||||
.mc-company-name { font-weight: bold; font-size: 5.2pt; }
|
|
||||||
.mc-text.mc-small { font-size: 4.6pt; }
|
|
||||||
|
|
||||||
.mc-box { border: 1px solid #000; box-sizing: border-box; }
|
|
||||||
.mc-title { position: absolute; font-weight: bold; font-size: 8pt; text-align: center; white-space: nowrap; }
|
|
||||||
.mc-barcode { position: absolute; text-align: center; }
|
|
||||||
.mc-barcode img { height: 26px; }
|
|
||||||
.mc-voucher { position: absolute; text-align: center; font-weight: bold; font-size: 10.5pt; letter-spacing: 0.5px; }
|
|
||||||
.mc-copylabel { position: absolute; text-align: center; font-weight: bold; font-size: 8pt; }
|
|
||||||
|
|
||||||
.mc-cell { border: 1px solid #000; box-sizing: border-box; padding: 1px 2px; }
|
|
||||||
.mc-cell-label { font-size: 5.5pt; }
|
|
||||||
.mc-cell-val { font-weight: bold; font-size: 8pt; margin-top: 3px; }
|
|
||||||
.mc-cell-service { text-align: center; }
|
|
||||||
|
|
||||||
/* 5-cell deposit/destination/weight/pieces/volumetric row: a real
|
|
||||||
table with border-collapse so each internal shared edge is
|
|
||||||
drawn once, not twice (see _main-copy.blade.php comment). */
|
|
||||||
.mc-cell-row { position: absolute; border-collapse: collapse; table-layout: fixed; }
|
|
||||||
.mc-cell-td { border: 1px solid #000; box-sizing: border-box; padding: 1px 2px; vertical-align: top; }
|
|
||||||
|
|
||||||
.mc-partybox { border: 1px solid #000; box-sizing: border-box; }
|
|
||||||
.mc-party-title { position: absolute; font-weight: bold; font-size: 9pt; }
|
|
||||||
.mc-party-line { position: absolute; font-size: 6.3pt; white-space: nowrap; overflow: hidden; }
|
|
||||||
|
|
||||||
.mc-charge { text-align: center; font-weight: bold; font-size: 8pt; box-sizing: border-box; padding-top: 2px; }
|
|
||||||
|
|
||||||
/* Payment stub (copy 3) reuses the .mc-* classes but at smaller
|
|
||||||
scale, matching the RDLC's own narrower band. */
|
|
||||||
.ps-brandbox { border: 1.5px solid #c8102e; box-sizing: border-box; text-align: center; padding-top: 4px; }
|
|
||||||
.ps-brand-logo img { width: 60%; margin: 0 auto 8px; display: block; }
|
|
||||||
.ps-brand-tag { color: #c8102e; font-weight: bold; font-size: 9pt; }
|
|
||||||
.ps-brandbox-small { border: 1px solid #000; box-sizing: border-box; text-align: center; padding: 2px; }
|
|
||||||
.ps-brand-logo-sm img { width: 40%; margin: 2px auto; display: block; }
|
|
||||||
.ps-legal-sm { font-size: 4.6pt; line-height: 1.3; }
|
|
||||||
|
|
||||||
/* Perforated tear-off strip on the right of copy 1 only, with
|
|
||||||
the rotated "3. ΑΡΧΕΙΟ / ΓΡΑΦΕΙΟ ΚΑΤΑΘΕΣΗΣ" label — RDLC's
|
|
||||||
image10 (x=15.42cm) is the dashed perforation line itself;
|
|
||||||
reproduced here as a CSS dashed border since we can't extract
|
|
||||||
that embedded bitmap. transform:rotate() (not
|
|
||||||
writing-mode:vertical-rl, confirmed broken in dompdf) rotates
|
|
||||||
a normal-flow block. */
|
|
||||||
.stub-strip { position: absolute; top: 2.25cm; left: 15.42cm; width: 5.58cm; height: 6.16cm; border-left: 1px dashed #000; }
|
|
||||||
.stub-vtext {
|
|
||||||
position: absolute; top: 2.6cm; left: -1.3cm; width: 6cm;
|
|
||||||
text-align: center; font-size: 6pt; font-weight: bold;
|
|
||||||
white-space: nowrap; transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-line { position: absolute; left: 0; width: 20.9cm; border-top: 1px dashed #000; text-align: center; font-weight: bold; font-size: 7pt; }
|
|
||||||
.cut-line span { position: relative; top: -5px; background: #fff; padding: 0 6px; }
|
|
||||||
|
|
||||||
.footer-ocr { position: absolute; top: 27.35cm; left: 0; width: 21cm; text-align: center; font-family: monospace; font-size: 8pt; letter-spacing: 1.5px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="page">
|
|
||||||
|
|
||||||
{{-- Copy 1: delivery copy — RDLC band top=0 --}}
|
|
||||||
<div class="band" style="top:0.05cm; width:15.75cm; height:8.7cm;">
|
|
||||||
@include('core::shipping.carriers.elta.partials._main-copy', ['copyType' => 'delivery'])
|
|
||||||
</div>
|
|
||||||
<div class="stub-strip">
|
|
||||||
<div class="stub-vtext">3. ΑΡΧΕΙΟ / ΓΡΑΦΕΙΟ ΚΑΤΑΘΕΣΗΣ</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cut-line" style="top:9.0cm;"><span>✂ ΑΠΟΚΟΨΤΕ ΕΔΩ</span></div>
|
|
||||||
|
|
||||||
{{-- Copy 2: sender's copy — RDLC band top≈8.94cm on the full page --}}
|
|
||||||
<div class="band" style="top:9.05cm; width:15.75cm; height:9.9cm;">
|
|
||||||
@include('core::shipping.carriers.elta.partials._main-copy', ['copyType' => 'sender'])
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cut-line" style="top:18.75cm;"><span>✂ ΑΠΟΚΟΨΤΕ ΕΔΩ</span></div>
|
|
||||||
|
|
||||||
{{-- Copy 3: ΤΑΧΥΠΛΗΡΩΜΗ ΕΙΣΠΡΑΞΗ / ΜΕΤΑΒΙΒΑΣΗ stub — RDLC band top≈19.22cm --}}
|
|
||||||
<div style="position:absolute; top:19.05cm; left:0.27cm; width:20.4cm; font-weight:bold; font-size:7pt; text-align:center;">
|
|
||||||
ΤΑΧΥΠΛΗΡΩΜΗ ΕΙΣΠΡΑΞΗ / ΜΕΤΑΒΙΒΑΣΗ Ο. Αριθμός Λογ/μού Ταχυπληρωμής {{ $siimvasi }}
|
|
||||||
</div>
|
|
||||||
<div class="band" style="top:19.45cm; width:16.75cm; height:7.4cm;">
|
|
||||||
@include('core::shipping.carriers.elta.partials._payment-stub')
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cut-line" style="top:26.85cm;"><span>ΜΗ ΣΗΜΕΙΩΝΕΤΕ ΚΑΤΩ ΑΠΟ ΑΥΤΗ ΤΗ ΓΡΑΜΜΗ</span></div>
|
|
||||||
|
|
||||||
<div class="footer-ocr">>{{ $ocr_line }}<</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="el">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
{{--
|
|
||||||
Geometry transcribed directly from ELTA_PEL.SydetaLabelE.rdlc
|
|
||||||
— the RDLC ELTA's own client selects for printer_size==2
|
|
||||||
(the A6/"thermal label" printer-setup radio button), per
|
|
||||||
Sydeta.cs::print_vg(); RDLCPrinter.cs overrides the actual
|
|
||||||
print DeviceInfo to 10.4cm x 14.8cm for this printer_size
|
|
||||||
regardless of what PageWidth/PageHeight the RDLC itself
|
|
||||||
declares (all ELTA RDLCs declare A4 internally). Every
|
|
||||||
.a6-* absolute position below is that report's own
|
|
||||||
Top/Left in cm.
|
|
||||||
--}}
|
|
||||||
@page { margin: 0; size: 10.4cm 14.8cm; }
|
|
||||||
html, body { margin: 0; padding: 0; }
|
|
||||||
body { font-family: 'DejaVu Sans', sans-serif; font-size: 6.3pt; color: #000; }
|
|
||||||
|
|
||||||
{{--
|
|
||||||
height is deliberately a hair under the true 14.8cm page —
|
|
||||||
dompdf's border-box math isn't quite exact at this scale, and
|
|
||||||
a .page box sized to exactly fill the page (even with
|
|
||||||
box-sizing:border-box) was empirically confirmed to overflow
|
|
||||||
a fraction of a point past the page canvas and silently push
|
|
||||||
a second, blank page (reproduced with an otherwise-empty
|
|
||||||
.page div: only the border's presence, not any content,
|
|
||||||
triggered it — 14.7cm was stable, 14.75cm was not).
|
|
||||||
--}}
|
|
||||||
.page { position: relative; width: 10.4cm; height: 14.65cm; border: 1.5px solid #000; box-sizing: border-box; overflow: hidden; }
|
|
||||||
.page > * { position: absolute; box-sizing: border-box; }
|
|
||||||
|
|
||||||
.a6-logo img { width: 100%; height: 100%; object-fit: contain; }
|
|
||||||
.a6-service-box { border: 1px solid #000; text-align: center; }
|
|
||||||
.a6-service-code { font-weight: bold; font-size: 7pt; }
|
|
||||||
.a6-service-name { font-weight: bold; font-size: 6pt; text-align: center; }
|
|
||||||
|
|
||||||
.a6-title { font-weight: bold; font-size: 7pt; }
|
|
||||||
.a6-datetime { font-size: 6pt; }
|
|
||||||
.a6-copy { font-weight: bold; font-size: 7pt; text-align: right; }
|
|
||||||
|
|
||||||
.a6-station-box { border: 1px solid #000; }
|
|
||||||
.a6-station-label { font-size: 6pt; font-weight: bold; }
|
|
||||||
.a6-station-val { font-weight: bold; font-size: 8pt; }
|
|
||||||
|
|
||||||
.a6-cell { border: 1px solid #000; padding: 1px 3px; }
|
|
||||||
.a6-cell-label { font-size: 6pt; }
|
|
||||||
.a6-cell-val { font-weight: bold; font-size: 8pt; }
|
|
||||||
|
|
||||||
/* ΧΡΕΩΣΗ/REFERENCE/ΣΥΜΒΑΣΗ row and the Γρ.Κατάθεσης/ΒΑΡΟΣ/
|
|
||||||
ΟΓΚΟΜΕΤΡΙΚΟ/Τεμάχια row: real tables with border-collapse so
|
|
||||||
each internal shared edge is drawn once (see label-a6.blade.php
|
|
||||||
comment above their markup). */
|
|
||||||
.a6-cell-row { position: absolute; border-collapse: collapse; border-spacing: 0; table-layout: fixed; }
|
|
||||||
.a6-cell-td { border: 1px solid #000; box-sizing: border-box; padding: 1px 3px; vertical-align: top; overflow: hidden; }
|
|
||||||
|
|
||||||
.a6-box { border: 1px solid #000; padding: 2px 3px; }
|
|
||||||
.a6-party-title { font-weight: bold; font-size: 7pt; }
|
|
||||||
.a6-party-line { font-size: 8pt; white-space: nowrap; overflow: hidden; }
|
|
||||||
|
|
||||||
.a6-return-title { font-weight: bold; font-size: 6pt; }
|
|
||||||
.a6-return-item { font-size: 6pt; }
|
|
||||||
.a6-checkbox { border: 1px solid #000; display: inline-block; width: 6px; height: 6px; margin-right: 3px; vertical-align: middle; }
|
|
||||||
|
|
||||||
.a6-small-title { font-weight: bold; font-size: 5pt; }
|
|
||||||
.a6-small-val { font-size: 6pt; }
|
|
||||||
|
|
||||||
.a6-legal { font-size: 5pt; text-align: center; line-height: 1.4; }
|
|
||||||
|
|
||||||
.a6-barcode { text-align: center; }
|
|
||||||
.a6-barcode img { height: 30px; }
|
|
||||||
.a6-voucher { text-align: center; font-weight: bold; font-size: 12pt; letter-spacing: 1px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="page">
|
|
||||||
{{-- Header: logo (0.10,0.12,1.66x1.26) + service box (6.42,0.10,3.93x0.84) --}}
|
|
||||||
<div class="a6-logo" style="top:0.12cm;left:0.10cm;width:1.66cm;height:1.26cm;">
|
|
||||||
<img src="{{ $eltaLogo }}" alt="ELTA Courier">
|
|
||||||
</div>
|
|
||||||
<div class="a6-service-box" style="top:0.10cm;left:6.42cm;width:3.93cm;height:0.84cm;">
|
|
||||||
<div class="a6-service-code" style="position:absolute;top:0.02cm;left:0.09cm;">ΥΠΗΡΕΣΙΑ: {{ $service_code }}</div>
|
|
||||||
<div class="a6-service-name" style="position:absolute;top:0.40cm;left:0.04cm;width:3.81cm;">{{ $service_name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="a6-licence-box" style="top:1.05cm;left:6.42cm;width:3.88cm;height:0.62cm;border:1px solid #000;text-align:center;font-size:5.3pt;font-weight:bold;line-height:1.3;padding-top:2px;">
|
|
||||||
ΕΕΤΤ ΑΜ: 99-150 Γενική Άδεια<br>Ταχ/κων Υπηρεσιών
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="a6-datetime" style="top:1.68cm;left:0.13cm;width:1.68cm;">{{ $date }}</div>
|
|
||||||
<div class="a6-title" style="top:1.79cm;left:1.93cm;width:5.89cm;">ΣΥΝΟΔΕΥΤΙΚΟ ΔΕΛΤΙΟ ΤΑΧΥΜΕΤΑΦΟΡΑΣ</div>
|
|
||||||
{{-- The RDLC's own barcode textbox here uses the "Free 3 of 9
|
|
||||||
Extended" barcode font — we don't have that font, so this
|
|
||||||
would-be *{voucher}* fallback text is dropped in favor of the
|
|
||||||
real Code 128 barcode image rendered near the bottom instead. --}}
|
|
||||||
<div class="a6-datetime" style="top:2.00cm;left:0.16cm;width:1.60cm;">{{ $time }}</div>
|
|
||||||
|
|
||||||
<div class="a6-station-box" style="top:2.33cm;left:0.10cm;width:10.20cm;height:0.44cm;">
|
|
||||||
<div class="a6-station-label" style="position:absolute;top:0.05cm;left:0.10cm;">Γρ. Επίδοσης :</div>
|
|
||||||
<div class="a6-station-val" style="position:absolute;top:0.03cm;left:2.50cm;">{{ $station_pros }}</div>
|
|
||||||
<div class="a6-station-val" style="position:absolute;top:0.03cm;left:3.90cm;width:6.06cm;">{{ $station_pros_title }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ΧΡΕΩΣΗ / REFERENCE / ΣΥΜΒΑΣΗ 3-cell row, and the Γρ.Κατάθεσης /
|
|
||||||
ΒΑΡΟΣ / ΟΓΚΟΜΕΤΡΙΚΟ ΒΑΡΟΣ / Τεμάχια row right below it: both are
|
|
||||||
genuinely tabular single rows of fixed-width cells, so — like
|
|
||||||
the equivalent A4 rows — they're built as real
|
|
||||||
<table border-collapse:collapse> instead of independently-
|
|
||||||
bordered absolute divs, which was drawing every shared edge
|
|
||||||
(each cell-to-cell seam, and the seam between these two rows)
|
|
||||||
twice, visibly doubling/thickening those lines versus a real
|
|
||||||
printed ELTA label. Each table is positioned at the row's own
|
|
||||||
RDLC top/left; the first table's bottom border is dropped since
|
|
||||||
the second table's top border already draws that shared line. --}}
|
|
||||||
<table class="a6-cell-row" style="top:2.82cm;left:0.10cm;width:10.16cm;">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width:5.41cm;"><col style="width:2.84cm;"><col style="width:1.91cm;">
|
|
||||||
</colgroup>
|
|
||||||
<tr>
|
|
||||||
<td class="a6-cell-td" style="height:0.47cm;border-bottom:none;">
|
|
||||||
<div style="font-size:7pt;">{{ $xreosi }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="a6-cell-td" style="height:0.47cm;text-align:center;border-bottom:none;">
|
|
||||||
<div style="font-size:7pt;">{{ $ocr_reference }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="a6-cell-td" style="height:0.47cm;text-align:center;border-bottom:none;">
|
|
||||||
<div style="font-size:7pt;">{{ $siimvasi }}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<table class="a6-cell-row" style="top:3.39cm;left:0.10cm;width:9.97cm;">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width:2.00cm;"><col style="width:2.00cm;"><col style="width:4.12cm;"><col style="width:1.85cm;">
|
|
||||||
</colgroup>
|
|
||||||
<tr>
|
|
||||||
<td class="a6-cell-td" style="height:0.97cm;">
|
|
||||||
<div class="a6-cell-label">Γρ.Κατάθεσης:</div>
|
|
||||||
<div class="a6-cell-val">{{ $station_apo }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="a6-cell-td" style="height:0.97cm;">
|
|
||||||
<div class="a6-cell-label">ΒΑΡΟΣ(Kgr)</div>
|
|
||||||
<div class="a6-cell-val">{{ $weight }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="a6-cell-td" style="height:0.97cm;">
|
|
||||||
<div class="a6-cell-label">ΟΓΚΟΜΕΤΡΙΚΟ ΒΑΡΟΣ(Kgr)</div>
|
|
||||||
<div class="a6-cell-val" style="font-size:7pt;">{{ $volumetric_weight ?? '0.000' }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="a6-cell-td" style="height:0.97cm;">
|
|
||||||
<div class="a6-cell-label">Τεμάχια</div>
|
|
||||||
<div class="a6-cell-val">{{ $package_label }}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{{-- ΑΠΟΣΤΟΛΕΑΣ + ΑΙΤΙΑ ΕΠΙΣΤΡΟΦΗΣ --}}
|
|
||||||
<div class="a6-box" style="top:4.43cm;left:0.10cm;width:7.24cm;height:2.44cm;">
|
|
||||||
@include('core::shipping.carriers.elta.partials._party-box', [
|
|
||||||
'title' => 'ΑΠΟΣΤΟΛΕΑΣ',
|
|
||||||
'lines' => $sender_lines,
|
|
||||||
])
|
|
||||||
</div>
|
|
||||||
<div class="a6-box" style="top:4.43cm;left:7.55cm;width:2.80cm;height:2.44cm;">
|
|
||||||
<div class="a6-return-title">ΑΙΤΙΑ ΕΠΙΣΤΡΟΦΗΣ</div>
|
|
||||||
<div style="margin-top:4px;">
|
|
||||||
<div class="a6-return-item"><span class="a6-checkbox"></span>Άγνωστος</div>
|
|
||||||
<div class="a6-return-item" style="margin-top:4px;"><span class="a6-checkbox"></span>Ελλειπή Δ/νση</div>
|
|
||||||
<div class="a6-return-item" style="margin-top:4px;"><span class="a6-checkbox"></span>Απαράδεκτο</div>
|
|
||||||
<div class="a6-return-item" style="margin-top:4px;"><span class="a6-checkbox"></span>Άλλαξε Δ/νση</div>
|
|
||||||
<div class="a6-return-item" style="margin-top:4px;"><span class="a6-checkbox"></span>Αζήτητο</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ΠΑΡΑΛΗΠΤΗΣ + ΑΝΤΙΚΑΤΑΒΟΛΗ column --}}
|
|
||||||
<div class="a6-box" style="top:6.97cm;left:0.10cm;width:7.26cm;height:2.78cm;">
|
|
||||||
@include('core::shipping.carriers.elta.partials._party-box', [
|
|
||||||
'title' => 'ΠΑΡΑΛΗΠΤΗΣ',
|
|
||||||
'lines' => $recipient_lines,
|
|
||||||
])
|
|
||||||
</div>
|
|
||||||
<div class="a6-box" style="top:6.97cm;left:7.48cm;width:2.85cm;height:2.79cm;">
|
|
||||||
@if ($antik_1 ?? null)
|
|
||||||
<div style="font-size:6pt;">{{ $antik_1 }}</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- REFERENCE / ΕΠΙΒΑΡΥΝΣΕΙΣ --}}
|
|
||||||
<div class="a6-box" style="top:9.84cm;left:0.10cm;width:5.81cm;height:0.69cm;">
|
|
||||||
<div class="a6-small-title">REFERENCE</div>
|
|
||||||
<div class="a6-small-val" style="margin-top:3px; font-weight:bold;">{{ $order_reference }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="a6-box" style="top:9.84cm;left:6.00cm;width:4.37cm;height:1.78cm;">
|
|
||||||
<div class="a6-small-title">* ΕΠΙΒΑΡΥΝΣΕΙΣ *</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- ΠΑΡΑΤΗΡΗΣΕΙΣ --}}
|
|
||||||
<div class="a6-box" style="top:10.60cm;left:0.10cm;width:5.82cm;height:1.01cm;">
|
|
||||||
<div class="a6-small-title">* ΠΑΡΑΤΗΡΗΣΕΙΣ *</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if ($multiPiece)
|
|
||||||
<div class="a6-box" style="top:11.68cm;left:0.10cm;width:10.23cm;height:0.54cm;text-align:center;">
|
|
||||||
<div style="font-weight:bold; font-size:9pt; margin-top:2px;">* ΠΟΛΛΑΠΛΗ ΑΠΟΣΤΟΛΗ * {{ $package_label }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="a6-box" style="top:12.20cm;left:0.11cm;width:10.23cm;height:0.57cm;">
|
|
||||||
<div class="a6-legal">
|
|
||||||
Ισχύουν οι Γενικοί Οροι Παραχής Υπηρεσιών οι οποίοι βρίσκονται αναρτημένοι στο www.elta-courier.gr<br>
|
|
||||||
και είναι διαθέσιμοι σε ολα τα καταστήματα της εταιρίας.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if ($antik_1 ?? null)
|
|
||||||
<div class="a6-box" style="top:12.70cm;left:0.10cm;width:10.23cm;height:0.51cm;text-align:center;">
|
|
||||||
<div style="font-weight:bold; font-size:9pt; margin-top:4px;">{{ $antik_1 }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="a6-barcode" style="top:13.20cm;left:0.10cm;width:10.27cm;">
|
|
||||||
<img src="{{ $barcode_voucher }}" alt="">
|
|
||||||
</div>
|
|
||||||
<div class="a6-voucher" style="top:13.95cm;left:0.10cm;width:10.30cm;">{{ $voucher_no }}</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
{{--
|
|
||||||
One "ΣΥΝΟΔΕΥΤΙΚΟ ΔΕΛΤΙΟ ΤΑΧΥΜΕΤΑΦΟΡΑΣ" copy (delivery or sender's),
|
|
||||||
geometry transcribed directly from ELTA_PEL.sydetaE.rdlc's delivery-
|
|
||||||
copy band (Top/Left values there are relative to that band; here
|
|
||||||
they're absolute cm offsets from THIS partial's own 0,0, since the
|
|
||||||
caller wraps it in a `position:relative` container sized 15.75cm x
|
|
||||||
8.75cm — the same width/height as the RDLC band, read off its own
|
|
||||||
rectangle8/rectangle9/rectangle11/rectangle12 extents).
|
|
||||||
|
|
||||||
Expects: $copyType ('delivery'|'sender') and all of
|
|
||||||
EltaLabelRenderer's $data.
|
|
||||||
--}}
|
|
||||||
<div class="mc-logo" style="top:0.09cm;left:0.32cm;width:2.65cm;height:1.93cm;">
|
|
||||||
<img src="{{ $eltaLogo }}" alt="ELTA Courier">
|
|
||||||
</div>
|
|
||||||
<div class="mc-company" style="top:0.12cm;left:3.15cm;width:5.19cm;">
|
|
||||||
<div class="mc-company-name">ΕΛΛΗΝΙΚΑ ΤΑΧΥΔΡΟΜΕΙΑ Α.Ε.</div>
|
|
||||||
<div>Έδρα: Απελλού 1, 105 51, Αθήνα</div>
|
|
||||||
<div>Υποκ/μα: Λ. Μεσογείων 395, 153 43, Αγ. Παρασκευή</div>
|
|
||||||
<div>Α.Φ.Μ.: 094026421 | Δ.Ο.Υ.: ΚΕΦΟ.Δ.Ε. ΑΤΤΙΚΗΣ</div>
|
|
||||||
<div>Τ. 210-6073000 | Ε. info@elta-courier.gr</div>
|
|
||||||
<div>www.eltacourier.gr</div>
|
|
||||||
</div>
|
|
||||||
<div class="mc-text mc-small" style="top:1.80cm;left:3.15cm;width:5.03cm;">ΕΕΤΤ ΑΜ 99-150 Γενική Αδεια Ταχ/κων Υπηρεσιών</div>
|
|
||||||
|
|
||||||
{{-- Title / barcode / voucher, centered column at x=8.52..15.58 --}}
|
|
||||||
<div class="mc-box" style="top:0.05cm;left:8.52cm;width:7.06cm;height:1.75cm;">
|
|
||||||
<div class="mc-title" style="top:0.19cm;left:0.05cm;width:6.85cm;">ΣΥΝΟΔΕΥΤΙΚΟ ΔΕΛΤΙΟ ΤΑΧΥΜΕΤΑΦΟΡΑΣ</div>
|
|
||||||
<div class="mc-barcode" style="top:0.57cm;left:0.24cm;width:6.67cm;">
|
|
||||||
<img src="{{ $barcode_voucher }}" alt="">
|
|
||||||
</div>
|
|
||||||
{{-- The RDLC's own barcode textbox uses the "Free 3 of 9 Extended"
|
|
||||||
barcode font (literal *{voucher}* text rendered as bars by that
|
|
||||||
font) — we don't have that font, so we render a real Code 128
|
|
||||||
barcode image above instead (visually equivalent, actually
|
|
||||||
scannable), making this second plain-text *{voucher}* line
|
|
||||||
redundant with it rather than a distinct field. --}}
|
|
||||||
<div class="mc-voucher" style="top:1.29cm;left:0.24cm;width:6.64cm;">{{ $voucher_no }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- 5-cell deposit/destination/weight/pieces/volumetric row — a genuinely
|
|
||||||
tabular single row of fixed-width cells, so it's built as a real
|
|
||||||
<table border-collapse:collapse> rather than independently-bordered
|
|
||||||
absolute divs: that was drawing every shared internal edge twice
|
|
||||||
(once per adjacent cell), producing a visibly doubled/thickened line
|
|
||||||
that real printed ELTA labels don't show. The table is positioned via
|
|
||||||
the same absolute top/left/width the RDLC geometry gives the row as a
|
|
||||||
whole; each <td> keeps its own RDLC-derived width via <col>. --}}
|
|
||||||
<table class="mc-cell-row" style="top:2.20cm;left:0.27cm;width:8.44cm;height:0.82cm;">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width:1.51cm;"><col style="width:1.67cm;"><col style="width:1.35cm;"><col style="width:1.08cm;"><col style="width:2.83cm;">
|
|
||||||
</colgroup>
|
|
||||||
<tr>
|
|
||||||
<td class="mc-cell-td">
|
|
||||||
<div class="mc-cell-label">Γρ.Κατάθεσης</div>
|
|
||||||
<div class="mc-cell-val">{{ $station_apo }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td">
|
|
||||||
<div class="mc-cell-label">Γρ.Προορισμού</div>
|
|
||||||
<div class="mc-cell-val">{{ $station_pros }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td">
|
|
||||||
<div class="mc-cell-label">Βάρος</div>
|
|
||||||
<div class="mc-cell-val">{{ $weight }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td">
|
|
||||||
<div class="mc-cell-label">Τεμάχια</div>
|
|
||||||
<div class="mc-cell-val">{{ $package_label }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td">
|
|
||||||
<div class="mc-cell-label">Ογκ/κο Βάρος</div>
|
|
||||||
<div class="mc-cell-val" style="font-size:7pt;">{{ $volumetric_weight ?? '0.000' }}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div class="mc-cell mc-cell-service" style="top:2.20cm;left:9.19cm;width:6.19cm;height:0.82cm;">
|
|
||||||
<div style="position:absolute;top:0.05cm;left:0.32cm;font-size:6.5pt;">ΥΠΗΡΕΣΙΑ :</div>
|
|
||||||
<div style="position:absolute;top:0.03cm;left:3.21cm;font-size:7.5pt;">{{ $service_code }}</div>
|
|
||||||
<div style="position:absolute;top:0.42cm;left:0.05cm;width:6.08cm;text-align:center;font-size:7.5pt;">{{ $service_name }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Sender box --}}
|
|
||||||
<div class="mc-box mc-partybox" style="top:3.12cm;left:0.27cm;width:6.24cm;height:2.53cm;">
|
|
||||||
<div class="mc-party-title" style="top:0.05cm;left:0.10cm;">ΑΠΟΣΤΟΛΕΑΣ</div>
|
|
||||||
@foreach ($sender_lines as $i => $line)
|
|
||||||
@if (trim((string) $line) !== '')
|
|
||||||
<div class="mc-party-line" style="top:{{ 0.53 + $i * 0.395 }}cm;left:0.05cm;width:6.11cm;">{{ $line }}</div>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Recipient box --}}
|
|
||||||
<div class="mc-box mc-partybox" style="top:5.76cm;left:0.29cm;width:6.20cm;height:2.68cm;">
|
|
||||||
<div class="mc-party-title" style="top:0.05cm;left:0.05cm;">ΠΑΡΑΛΗΠΤΗΣ</div>
|
|
||||||
@foreach ($recipient_lines as $i => $line)
|
|
||||||
@if (trim((string) $line) !== '')
|
|
||||||
<div class="mc-party-line" style="top:{{ 0.54 + $i * 0.445 }}cm;left:0.05cm;width:6.11cm;">{{ $line }}</div>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Charge banner + right-column panel stack (ΧΡΕΩΣΗ / Συν.Χρέωσης /
|
|
||||||
REFERENCE / ΠΑΡΑΤΗΡΗΣΕΙΣ, and the Πρόσθετες Υπηρεσίες column beside
|
|
||||||
them): these panels sit only a hair apart (~0.09-0.10cm, the RDLC's
|
|
||||||
own geometry, left untouched), close enough that each pair's two
|
|
||||||
independent borders read as one thick/doubled line at print
|
|
||||||
resolution. Each panel below keeps only the border sides it "owns"
|
|
||||||
on a shared seam (border-top/border-left removed where the
|
|
||||||
neighbouring panel above/left already draws that same line), so
|
|
||||||
every seam is drawn exactly once while every panel's outward-facing
|
|
||||||
sides keep their border. --}}
|
|
||||||
<div class="mc-box mc-charge" style="top:3.09cm;left:6.60cm;width:8.78cm;height:0.48cm;">{{ str_replace(' ΠΙΣΤΩΣΗ', ' ΤΡ.ΠΛΗΡ: ΠΙΣΤΩΣΗ', $xreosi) }}</div>
|
|
||||||
|
|
||||||
<div class="mc-box" style="top:3.66cm;left:6.60cm;width:4.37cm;height:0.44cm;border-top:none;">
|
|
||||||
<div style="position:absolute;top:0.05cm;left:0.05cm;font-size:7pt;font-weight:bold;">Συν.Χρέωσης (€):</div>
|
|
||||||
</div>
|
|
||||||
<div class="mc-box" style="top:3.66cm;left:11.07cm;width:4.31cm;height:2.65cm;border-top:none;">
|
|
||||||
<div style="position:absolute;top:0.04cm;left:0.05cm;font-size:6pt;">Πρόσθετες Υπηρεσίες</div>
|
|
||||||
@foreach ([$sur_1 ?? null, $sur_2 ?? null, $sur_3 ?? null, $sur_4 ?? null] as $i => $sur)
|
|
||||||
@if (!empty($sur))
|
|
||||||
<div style="position:absolute;top:{{ 0.35 + $i * 0.365 }}cm;left:0.08cm;width:4.18cm;font-size:6pt;">{{ $sur }}</div>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mc-box" style="top:4.20cm;left:6.59cm;width:4.37cm;height:0.67cm;border-top:none;border-right:none;">
|
|
||||||
<div style="position:absolute;top:0.03cm;left:0.05cm;font-size:6pt;">* REFERENCE No*</div>
|
|
||||||
<div style="position:absolute;top:0.31cm;left:0.05cm;font-size:6.5pt;">{{ $order_reference }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mc-box" style="top:4.97cm;left:6.60cm;width:4.37cm;height:1.31cm;border-top:none;">
|
|
||||||
<div style="position:absolute;top:0.03cm;left:0.08cm;font-size:6pt;">* ΠΑΡΑΤΗΡΗΣΕΙΣ *</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Right-hand signature / print-timestamp column, differs by copy type --}}
|
|
||||||
@if ($copyType === 'delivery')
|
|
||||||
<div class="mc-box" style="top:6.37cm;left:6.61cm;width:4.71cm;height:2.07cm;">
|
|
||||||
<div style="position:absolute;top:0.05cm;left:0.08cm;font-size:6.5pt;">{{ $antik_1 ?? '' }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mc-box" style="top:6.37cm;left:11.48cm;width:3.89cm;height:2.03cm;">
|
|
||||||
<div style="position:absolute;top:0.08cm;left:0.08cm;font-size:6pt;">ΓΙΑ ΤΗΝ ΠΑΡΑΛΑΒΗ</div>
|
|
||||||
<div style="position:absolute;top:0.38cm;left:0.08cm;font-size:6pt;">ΟΝΟΜΑ/ΥΠΟΓΡΑΦΗ</div>
|
|
||||||
<div style="position:absolute;top:1.73cm;left:0.11cm;font-size:6pt;">{{ $date }} {{ $time }}</div>
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div class="mc-box" style="top:6.37cm;left:6.61cm;width:4.71cm;height:2.07cm;">
|
|
||||||
<div style="position:absolute;top:0.05cm;left:0.08cm;font-size:6.5pt;">{{ $antik_1 ?? '' }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mc-box" style="top:6.37cm;left:11.48cm;width:3.89cm;height:2.03cm;">
|
|
||||||
<div style="position:absolute;top:0.08cm;left:0.08cm;font-size:6pt;font-weight:bold;">ΥΠΟΓΡΑΦΗ ΑΠΟΣΤΟΛΕΑ</div>
|
|
||||||
<div style="position:absolute;top:1.73cm;left:0.11cm;font-size:6pt;">Ημερομηνία - Ωρα Εκτύπωσης: {{ $date }} {{ $time }}</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{{--
|
|
||||||
Sender/recipient box, matching SydetaLabelE.rdlc's rectangle10/
|
|
||||||
rectangle11 (ΑΠΟΣΤΟΛΕΑΣ/ΠΑΡΑΛΗΠΤΗΣ) layout: a bold title line, then
|
|
||||||
the RDLC's sender_1..5 / rec_1..5 fields as separate plain text
|
|
||||||
lines (ELTA's own client pre-wraps the address into these fixed-
|
|
||||||
width lines server-side, so we render each line separately rather
|
|
||||||
than reflowing the address ourselves, to match the real line
|
|
||||||
breaks).
|
|
||||||
|
|
||||||
Expects: $title, $lines (array of up to 5 strings).
|
|
||||||
--}}
|
|
||||||
<div class="pb-title">{{ $title }}</div>
|
|
||||||
@foreach ($lines as $line)
|
|
||||||
@if (trim((string) $line) !== '')
|
|
||||||
<div class="pb-line">{{ $line }}</div>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
{{--
|
|
||||||
Copy 3: the ΤΑΧΥΠΛΗΡΩΜΗ ΕΙΣΠΡΑΞΗ/ΜΕΤΑΒΙΒΑΣΗ payment-receipt stub,
|
|
||||||
geometry transcribed from sydetaE.rdlc's third band (source Top
|
|
||||||
values there run ~19.22cm-27.0cm on the full A4 page; offsets below
|
|
||||||
are relative to this partial's own container, i.e. the RDLC's Top
|
|
||||||
minus 19.22cm). This band is narrower than copies 1/2 (starts at
|
|
||||||
x=3.01cm instead of x=0.27cm) — the RDLC leaves its left margin for
|
|
||||||
the perforated tear line + rotated "3. ΑΡΧΕΙΟ / ΓΡΑΦΕΙΟ ΚΑΤΑΘΕΣΗΣ"
|
|
||||||
text, reproduced by the caller outside this partial.
|
|
||||||
|
|
||||||
Expects all of EltaLabelRenderer's $data.
|
|
||||||
--}}
|
|
||||||
<div class="mc-box" style="top:0.00cm;left:6.44cm;width:6.64cm;height:1.15cm;">
|
|
||||||
<div class="mc-barcode" style="top:0.10cm;left:0.22cm;width:6.27cm;">
|
|
||||||
<img src="{{ $barcode_voucher }}" alt="">
|
|
||||||
</div>
|
|
||||||
<div class="mc-voucher" style="top:0.75cm;left:0.24cm;width:6.14cm;">{{ $voucher_no }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mc-copylabel" style="position:absolute;top:0.10cm;left:0.00cm;width:3.92cm;font-size:8pt;font-weight:bold;">{{ $voucher_no }}</div>
|
|
||||||
|
|
||||||
{{-- 4-cell deposit/destination/weight/pieces row: same doubled-border
|
|
||||||
issue and same fix as _main-copy.blade.php's 5-cell row — a real
|
|
||||||
<table border-collapse:collapse> in place of 4 independently-bordered
|
|
||||||
absolute divs, positioned at the row's own RDLC top/left/width. --}}
|
|
||||||
<table class="mc-cell-row" style="top:1.60cm;left:0.02cm;width:5.61cm;height:0.82cm;">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width:1.51cm;"><col style="width:1.67cm;"><col style="width:1.35cm;"><col style="width:1.08cm;">
|
|
||||||
</colgroup>
|
|
||||||
{{-- border-bottom:none on every cell: the ΑΠΟΣΤΟΛΕΑΣ party box
|
|
||||||
immediately below (top:2.44cm, this row ends at 2.42cm) already
|
|
||||||
draws that shared line with its own border-top. --}}
|
|
||||||
<tr>
|
|
||||||
<td class="mc-cell-td" style="border-bottom:none;">
|
|
||||||
<div class="mc-cell-label">Γρ.Κατάθεσης</div>
|
|
||||||
<div class="mc-cell-val">{{ $station_apo }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td" style="border-bottom:none;">
|
|
||||||
<div class="mc-cell-label">Γρ.Προορισμού</div>
|
|
||||||
<div class="mc-cell-val">{{ $station_pros }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td" style="border-bottom:none;">
|
|
||||||
<div class="mc-cell-label">Βάρος</div>
|
|
||||||
<div class="mc-cell-val">{{ $weight }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mc-cell-td" style="border-bottom:none;">
|
|
||||||
<div class="mc-cell-label">Τεμάχια</div>
|
|
||||||
<div class="mc-cell-val">{{ $package_label }}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div class="mc-box" style="top:1.60cm;left:5.90cm;width:4.87cm;height:0.40cm;">
|
|
||||||
<div style="position:absolute;top:0.05cm;left:0.13cm;font-size:6pt;">{{ $service_code }}</div>
|
|
||||||
<div style="position:absolute;top:0.03cm;left:0.85cm;width:3.89cm;font-size:6pt;">{{ $service_name }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Below: several panel pairs sit a hair apart (or, for the
|
|
||||||
Συν.Χρέωσης/ΕΠΙΒΑΡΥΝΣΕΙΣ pair, even overlap slightly) per the RDLC's
|
|
||||||
own geometry — left untouched — close enough that two independent
|
|
||||||
borders read as one doubled/thick line. border-top/border-bottom is
|
|
||||||
removed from one side of each pair so only the remaining box's
|
|
||||||
border draws that shared line. --}}
|
|
||||||
<div class="mc-box mc-charge" style="top:2.02cm;left:5.90cm;width:4.87cm;height:0.37cm;font-size:5.4pt;padding-top:4px;white-space:nowrap;overflow:hidden;border-top:none;">{{ str_replace(' ΠΙΣΤΩΣΗ', ' ΤΡ.ΠΛΗΡ: ΠΙΣΤΩΣΗ', $xreosi) }}</div>
|
|
||||||
|
|
||||||
<div class="mc-box mc-partybox" style="top:2.44cm;left:0.00cm;width:5.98cm;height:0.91cm;overflow:hidden;">
|
|
||||||
<div class="mc-party-title" style="top:0.03cm;left:0.10cm;font-size:6.5pt;">ΑΠΟΣΤΟΛΕΑΣ</div>
|
|
||||||
@foreach ($sender_lines as $i => $line)
|
|
||||||
@if (trim((string) $line) !== '')
|
|
||||||
<div class="mc-party-line" style="top:{{ 0.30 + $i * 0.19 }}cm;left:0.06cm;width:5.85cm;font-size:5.6pt;">{{ $line }}</div>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<div class="mc-box" style="top:2.45cm;left:6.06cm;width:4.71cm;height:1.17cm;border-bottom:none;">
|
|
||||||
<div style="position:absolute;top:0.02cm;left:0.16cm;font-size:6pt;">Συν.Χρέωσης (€):</div>
|
|
||||||
<div style="position:absolute;top:0.30cm;left:0.16cm;font-size:6pt;">Πρόσθετες Υπηρεσίες</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mc-box mc-partybox" style="top:3.35cm;left:0.00cm;width:5.97cm;height:1.63cm;overflow:hidden;border-top:none;">
|
|
||||||
<div class="mc-party-title" style="top:0.06cm;left:0.10cm;font-size:6.5pt;">ΠΑΡΑΛΗΠΤΗΣ</div>
|
|
||||||
@foreach ($recipient_lines as $i => $line)
|
|
||||||
@if (trim((string) $line) !== '')
|
|
||||||
<div class="mc-party-line" style="top:{{ 0.34 + $i * 0.26 }}cm;left:0.10cm;width:5.79cm;font-size:5.6pt;">{{ $line }}</div>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<div class="mc-box" style="top:3.28cm;left:6.06cm;width:4.71cm;height:1.41cm;">
|
|
||||||
<div style="position:absolute;top:0.05cm;left:0.14cm;font-size:6pt;">* ΕΠΙΒΑΡΥΝΣΕΙΣ *</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="position:absolute;top:4.95cm;left:0.05cm;width:8.75cm;font-size:4.6pt;line-height:1.25;white-space:nowrap;">
|
|
||||||
Έλαβα γνώση των όρων που αναγράφονται στο αντίγραφο 1 και 6 και τους αποδέχομαι ανεπιφύλακτα
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- rectangle29 --}}
|
|
||||||
<div class="mc-box" style="top:5.46cm;left:0.03cm;width:2.75cm;height:1.66cm;">
|
|
||||||
<div style="position:absolute;top:0.08cm;left:0.08cm;font-size:6pt;font-weight:bold;">ΥΠΟΓΡΑΦΗ ΑΠΟΣΤΟΛΕΑ</div>
|
|
||||||
<div style="position:absolute;top:1.35cm;left:0.08cm;font-size:6pt;">{{ $date }}</div>
|
|
||||||
</div>
|
|
||||||
{{-- rectangle43 --}}
|
|
||||||
<div class="mc-box" style="top:5.47cm;left:2.89cm;width:3.76cm;height:1.66cm;">
|
|
||||||
<div style="position:absolute;top:0.09cm;left:0.08cm;font-size:6pt;font-weight:bold;">ΟΝΟΜΑ/ΥΠΟΓΡΑΦΗ ΠΑΡΑΛΗΠΤΗ</div>
|
|
||||||
<div style="position:absolute;top:1.35cm;left:0.08cm;font-size:6pt;">Ημ/νία: Ώρα:</div>
|
|
||||||
</div>
|
|
||||||
{{-- rectangle44 --}}
|
|
||||||
<div class="mc-box" style="top:5.48cm;left:6.76cm;width:3.99cm;height:1.68cm;text-align:center;">
|
|
||||||
<div style="position:absolute;top:0.04cm;left:0;width:100%;font-size:7pt;">Π Ο Σ Ο</div>
|
|
||||||
<div style="position:absolute;top:0.75cm;left:0;width:100%;font-size:13pt;font-weight:bold;">{{ number_format($cod_amount ?? 0, 2) }}</div>
|
|
||||||
</div>
|
|
||||||
{{-- rectangle45: small ELTA-branded legal box; the real RDLC fills it via
|
|
||||||
its own Image18 bitmap (baked "ΠΟΡΤΑ-ΠΟΡΤΑ ΕΛΛΗΝΙΚΑ ΤΑΧΥΔΡΟΜΕΙΑ..."
|
|
||||||
legal text + logo graphic) rather than text fields — approximated
|
|
||||||
here with our own extracted logo + the same legal text as plain
|
|
||||||
HTML, since we can't extract that embedded bitmap. --}}
|
|
||||||
<div class="mc-box ps-brandbox-small" style="top:5.55cm;left:10.84cm;width:2.72cm;height:1.61cm;">
|
|
||||||
<div class="ps-brand-logo-sm"><img src="{{ $eltaLogo }}" alt=""></div>
|
|
||||||
<div class="ps-legal-sm">
|
|
||||||
<strong>ΠΟΡΤΑ-ΠΟΡΤΑ</strong><br>
|
|
||||||
ΕΛΛΗΝΙΚΑ ΤΑΧΥΔΡΟΜΕΙΑ Α.Ε.<br>
|
|
||||||
ΕΕΤΤ ΑΜ: 99-150
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -65,7 +65,6 @@ class CorePlugin implements Plugin
|
|||||||
->brandName('Boboko')
|
->brandName('Boboko')
|
||||||
->brandLogo(asset('static/logos/core/boboko-logo.svg'))
|
->brandLogo(asset('static/logos/core/boboko-logo.svg'))
|
||||||
->darkModeBrandLogo(asset('static/logos/core/boboko-logo-white.svg'))
|
->darkModeBrandLogo(asset('static/logos/core/boboko-logo-white.svg'))
|
||||||
->favicon(asset('static/logos/core/favicon.svg'))
|
|
||||||
->login(Login::class)
|
->login(Login::class)
|
||||||
->resources([
|
->resources([
|
||||||
LanguageLineResource::class,
|
LanguageLineResource::class,
|
||||||
|
|||||||
@@ -39,8 +39,26 @@ class TransactionRecorder
|
|||||||
* elsewhere in this codebase (see the old, now-removed
|
* elsewhere in this codebase (see the old, now-removed
|
||||||
* TransactionRecorder this replaces).
|
* TransactionRecorder this replaces).
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Idempotent on (order_id, type, reference): a successful payment
|
||||||
|
* outcome can legitimately be reported twice for the same gateway
|
||||||
|
* reference — e.g. Stripe's pay()/handleCallback() both call
|
||||||
|
* resultFromIntent() and both dispatch PaymentCaptured once a
|
||||||
|
* PaymentIntent reaches "succeeded" (checkout's synchronous capture,
|
||||||
|
* then the webhook confirming the same outcome asynchronously) — so
|
||||||
|
* this returns the existing row instead of writing a duplicate.
|
||||||
|
*/
|
||||||
public function record(Order $order, string $type, string $driver, PaymentResult $result): Transaction
|
public function record(Order $order, string $type, string $driver, PaymentResult $result): Transaction
|
||||||
{
|
{
|
||||||
|
$existing = $order->transactions()
|
||||||
|
->where('type', $type)
|
||||||
|
->where('reference', $result->reference)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing !== null) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
return $order->transactions()->create([
|
return $order->transactions()->create([
|
||||||
'success' => $result->status === PaymentResultStatus::Succeeded,
|
'success' => $result->status === PaymentResultStatus::Succeeded,
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ class StripeWebhookMiddleware
|
|||||||
$secret
|
$secret
|
||||||
);
|
);
|
||||||
} catch (UnexpectedValueException|SignatureVerificationException $e) {
|
} catch (UnexpectedValueException|SignatureVerificationException $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Stripe webhook signature verification failed', [
|
||||||
|
'signature_header' => $stripeSig,
|
||||||
|
'secret_prefix' => substr((string) $secret, 0, 12),
|
||||||
|
'secret_length' => strlen((string) $secret),
|
||||||
|
'body_length' => strlen($request->getContent()),
|
||||||
|
'body_sha256' => hash('sha256', $request->getContent()),
|
||||||
|
'body_raw' => $request->getContent(),
|
||||||
|
'content_type' => $request->header('Content-Type'),
|
||||||
|
'content_encoding' => $request->header('Content-Encoding'),
|
||||||
|
]);
|
||||||
|
|
||||||
abort(400, $e->getMessage());
|
abort(400, $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ use Modules\Core\Shipping\Carriers\Acs\Jobs\WarmAcsAreaCacheJob;
|
|||||||
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowClient;
|
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowClient;
|
||||||
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowFulfillmentService;
|
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowFulfillmentService;
|
||||||
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver;
|
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowRateDriver;
|
||||||
use Modules\Core\Shipping\Carriers\Elta\EltaClient;
|
|
||||||
use Modules\Core\Shipping\Carriers\Elta\EltaFulfillmentService;
|
|
||||||
use Modules\Core\Shipping\Carriers\Elta\EltaRateDriver;
|
|
||||||
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
||||||
use Modules\Core\Shipping\Filament\Pages\ManageShippingRates;
|
use Modules\Core\Shipping\Filament\Pages\ManageShippingRates;
|
||||||
use Modules\Core\Shipping\Carriers\StorePickup\StorePickupRateDriver;
|
use Modules\Core\Shipping\Carriers\StorePickup\StorePickupRateDriver;
|
||||||
@@ -42,17 +39,14 @@ class ShippingServiceProvider extends ServiceProvider
|
|||||||
{
|
{
|
||||||
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/acs.php', 'acs');
|
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/acs.php', 'acs');
|
||||||
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/boxnow.php', 'boxnow');
|
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/boxnow.php', 'boxnow');
|
||||||
$this->mergeConfigFrom(__DIR__ . '/../../config/shippingCarriers/elta.php', 'elta');
|
|
||||||
|
|
||||||
$this->app->singleton(AcsClient::class, fn () => new AcsClient(config('acs')));
|
$this->app->singleton(AcsClient::class, fn () => new AcsClient(config('acs')));
|
||||||
$this->app->singleton(BoxNowClient::class, fn () => new BoxNowClient(config('boxnow')));
|
$this->app->singleton(BoxNowClient::class, fn () => new BoxNowClient(config('boxnow')));
|
||||||
$this->app->singleton(EltaClient::class, fn () => new EltaClient(config('elta')));
|
|
||||||
|
|
||||||
$this->app->bind(CarrierFulfillmentInterface::class, function ($app, array $params) {
|
$this->app->bind(CarrierFulfillmentInterface::class, function ($app, array $params) {
|
||||||
return match ($params['carrier'] ?? null) {
|
return match ($params['carrier'] ?? null) {
|
||||||
'acs' => $app->make(AcsFulfillmentService::class),
|
'acs' => $app->make(AcsFulfillmentService::class),
|
||||||
'box-now' => $app->make(BoxNowFulfillmentService::class),
|
'box-now' => $app->make(BoxNowFulfillmentService::class),
|
||||||
'elta' => $app->make(EltaFulfillmentService::class),
|
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -73,7 +67,6 @@ class ShippingServiceProvider extends ServiceProvider
|
|||||||
$this->publishes([
|
$this->publishes([
|
||||||
__DIR__ . '/../../config/shippingCarriers/acs.php' => config_path('shippingCarriers/acs.php'),
|
__DIR__ . '/../../config/shippingCarriers/acs.php' => config_path('shippingCarriers/acs.php'),
|
||||||
__DIR__ . '/../../config/shippingCarriers/boxnow.php' => config_path('shippingCarriers/boxnow.php'),
|
__DIR__ . '/../../config/shippingCarriers/boxnow.php' => config_path('shippingCarriers/boxnow.php'),
|
||||||
__DIR__ . '/../../config/shippingCarriers/elta.php' => config_path('shippingCarriers/elta.php'),
|
|
||||||
], 'core-config');
|
], 'core-config');
|
||||||
|
|
||||||
// Signed-URL auth only, same model as Lunar's own vendor
|
// Signed-URL auth only, same model as Lunar's own vendor
|
||||||
@@ -144,7 +137,6 @@ class ShippingServiceProvider extends ServiceProvider
|
|||||||
Shipping::extend('acs', fn ($app) => $app->make(AcsRateDriver::class));
|
Shipping::extend('acs', fn ($app) => $app->make(AcsRateDriver::class));
|
||||||
Shipping::extend('box-now', fn ($app) => $app->make(BoxNowRateDriver::class));
|
Shipping::extend('box-now', fn ($app) => $app->make(BoxNowRateDriver::class));
|
||||||
Shipping::extend('store-pickup', fn ($app) => $app->make(StorePickupRateDriver::class));
|
Shipping::extend('store-pickup', fn ($app) => $app->make(StorePickupRateDriver::class));
|
||||||
Shipping::extend('elta', fn ($app) => $app->make(EltaRateDriver::class));
|
|
||||||
|
|
||||||
$this->app->make(ConsoleSchedule::class)
|
$this->app->make(ConsoleSchedule::class)
|
||||||
->job(new WarmAcsAreaCacheJob)
|
->job(new WarmAcsAreaCacheJob)
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a Greek postcode to its ELTA station code/name via PELTKNEW
|
|
||||||
* (the *NEW-family replacement for the old, differently-authenticated
|
|
||||||
* PELSTATION — see EltaClient's docblock). Unlike Acs\AreaResolver,
|
|
||||||
* there's no daily bulk-warm job — PELTKNEW only accepts a single pel_tk
|
|
||||||
* (postcode), with no fetch-all variant to warm a whole-country cache
|
|
||||||
* from, so this caches per-postcode on first lookup instead.
|
|
||||||
*/
|
|
||||||
class AreaResolver
|
|
||||||
{
|
|
||||||
public function __construct(private readonly EltaClient $client) {}
|
|
||||||
|
|
||||||
public function resolve(string $postcode): EltaStation
|
|
||||||
{
|
|
||||||
return Cache::remember(
|
|
||||||
"elta.station.{$postcode}",
|
|
||||||
now()->addDays(30),
|
|
||||||
function () use ($postcode) {
|
|
||||||
$response = $this->client->getStation([
|
|
||||||
'pel_tk' => $postcode,
|
|
||||||
'sender_station' => '',
|
|
||||||
])->throwIfError();
|
|
||||||
|
|
||||||
$fields = $response->data['pel_fields'] ?? [];
|
|
||||||
|
|
||||||
return new EltaStation(
|
|
||||||
code: $fields['rec_station'] ?? '',
|
|
||||||
name: $fields['rec_station_t'] ?? '',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
use DOMDocument;
|
|
||||||
use DOMElement;
|
|
||||||
use DOMXPath;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Modules\Core\Shipping\Carriers\Elta\Exceptions\EltaApiException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SOAP transport for ELTA Courier's web services.
|
|
||||||
*
|
|
||||||
* ELTA runs two parallel operation families at the same endpoint
|
|
||||||
* (212.205.47.226:9003). The original CREATEAWB/PELB64VG/PELTT01/PELSTATION
|
|
||||||
* set (sourced from the unofficial chinchillabrains/eltaws reference client)
|
|
||||||
* rejects this account's credentials with st_flag=3 ("invalid password").
|
|
||||||
* Decompiling ELTA's own official desktop client (ELTA_PEL.exe, from their
|
|
||||||
* public .msi installer) showed it never calls CREATEAWB at all — it
|
|
||||||
* exclusively uses a newer "*NEW" family (PELVGNEW, PELLOGINNEW,
|
|
||||||
* PELTTNEW01, PELTKNEW, PELVGDEL, PELPARALVGNEW1). PELVGNEW was
|
|
||||||
* live-verified directly against the production endpoint with this same
|
|
||||||
* account and succeeded (st_flag=0, real voucher number returned) — it
|
|
||||||
* requires no password at all, just the account/user code pair.
|
|
||||||
*
|
|
||||||
* No .wsdl files exist for the *NEW family (only reconstructed from the
|
|
||||||
* decompiled C# proxy classes' method signatures). PHP's SoapClient in
|
|
||||||
* non-WSDL mode refuses to serialize these calls at all (SoapFault "Error
|
|
||||||
* in client request message" regardless of style/use/SoapVar wrapping —
|
|
||||||
* this legacy Micro Focus/CICS gateway's document/literal shape doesn't
|
|
||||||
* match what ext-soap's non-WSDL client is willing to build), so *NEW
|
|
||||||
* operations are sent as hand-built SOAP 1.1 envelopes over plain HTTP —
|
|
||||||
* proven directly against the live endpoint — with the response parsed
|
|
||||||
* back via DOMDocument/DOMXPath.
|
|
||||||
*
|
|
||||||
* The old family's label operation (PELB64VG) is deliberately NOT
|
|
||||||
* exposed here — it fails with the same st_flag=3 as CREATEAWB (same
|
|
||||||
* legacy family), and has no *NEW replacement since ELTA's own client
|
|
||||||
* renders labels locally instead of fetching them (see
|
|
||||||
* EltaLabelRenderer).
|
|
||||||
*/
|
|
||||||
class EltaClient
|
|
||||||
{
|
|
||||||
public function __construct(private readonly array $config) {}
|
|
||||||
|
|
||||||
public function login(array $params): EltaResponse
|
|
||||||
{
|
|
||||||
return $this->call('PELLOGINNEW', $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createVoucher(array $params): EltaResponse
|
|
||||||
{
|
|
||||||
return $this->call('PELVGNEW', $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function listVouchers(array $params): EltaResponse
|
|
||||||
{
|
|
||||||
return $this->call('PELPARALVGNEW1', $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deleteVoucher(array $params): EltaResponse
|
|
||||||
{
|
|
||||||
return $this->call('PELVGDEL', $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTracking(array $params): EltaResponse
|
|
||||||
{
|
|
||||||
return $this->call('PELTTNEW01', $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getStation(array $params): EltaResponse
|
|
||||||
{
|
|
||||||
return $this->call('PELTKNEW', $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function call(string $operation, array $params): EltaResponse
|
|
||||||
{
|
|
||||||
$namespace = '/'.$operation;
|
|
||||||
|
|
||||||
$body = '';
|
|
||||||
foreach ($params as $key => $value) {
|
|
||||||
$body .= '<cre:'.$key.'>'.htmlspecialchars((string) $value, ENT_XML1).'</cre:'.$key.'>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$envelope = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
||||||
.'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cre="'.$namespace.'">'
|
|
||||||
.'<soapenv:Header/><soapenv:Body><cre:READ>'.$body.'</cre:READ></soapenv:Body></soapenv:Envelope>';
|
|
||||||
|
|
||||||
$response = Http::withHeaders([
|
|
||||||
'SOAPAction' => 'Get',
|
|
||||||
'Content-Type' => 'text/xml',
|
|
||||||
])
|
|
||||||
->timeout($this->config['timeout'])
|
|
||||||
->withBody($envelope, 'text/xml')
|
|
||||||
->post($this->config['service_location']);
|
|
||||||
|
|
||||||
if ($response->failed()) {
|
|
||||||
throw new EltaApiException('ELTA request failed: HTTP '.$response->status());
|
|
||||||
}
|
|
||||||
|
|
||||||
return EltaResponse::fromSoapResult($this->parseEnvelope($response->body()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseEnvelope(string $xml): array
|
|
||||||
{
|
|
||||||
$dom = new DOMDocument();
|
|
||||||
|
|
||||||
if (! $dom->loadXML($xml)) {
|
|
||||||
throw new EltaApiException('ELTA returned a response that could not be parsed as XML.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$xpath = new DOMXPath($dom);
|
|
||||||
$xpath->registerNamespace('soapenv', 'http://schemas.xmlsoap.org/soap/envelope/');
|
|
||||||
$readResponse = $xpath->query('//soapenv:Body/*')->item(0);
|
|
||||||
|
|
||||||
if (! $readResponse instanceof DOMElement) {
|
|
||||||
throw new EltaApiException('ELTA response did not contain a SOAP body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->nodeToArray($readResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function nodeToArray(DOMElement $element): array
|
|
||||||
{
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
foreach ($element->childNodes as $child) {
|
|
||||||
if (! $child instanceof DOMElement) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = $child->localName;
|
|
||||||
$hasElementChildren = false;
|
|
||||||
|
|
||||||
foreach ($child->childNodes as $grandchild) {
|
|
||||||
if ($grandchild instanceof DOMElement) {
|
|
||||||
$hasElementChildren = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $hasElementChildren ? $this->nodeToArray($child) : trim($child->textContent);
|
|
||||||
|
|
||||||
if (array_key_exists($name, $data)) {
|
|
||||||
if (! is_array($data[$name]) || ! array_is_list($data[$name])) {
|
|
||||||
$data[$name] = [$data[$name]];
|
|
||||||
}
|
|
||||||
$data[$name][] = $value;
|
|
||||||
} else {
|
|
||||||
$data[$name] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Lunar\Models\Order;
|
|
||||||
use Modules\Core\Shipping\Carriers\Elta\Exceptions\EltaApiException;
|
|
||||||
use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface;
|
|
||||||
use Modules\Core\Shipping\Contracts\SupportsTracking;
|
|
||||||
use Modules\Core\Shipping\DTOs\ShipmentRequest;
|
|
||||||
use Modules\Core\Shipping\DTOs\TrackingCheckpoint;
|
|
||||||
use Modules\Core\Shipping\Enums\TrackingStatus;
|
|
||||||
use Modules\Core\Shipping\Models\Shipment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not SupportsManifestBatching — no manifest/pickup-list operation was
|
|
||||||
* found in either ELTA operation family, unlike ACS's
|
|
||||||
* ACS_Issue_Pickup_List. Same shape as Box Now, which has no manifest
|
|
||||||
* step either.
|
|
||||||
*/
|
|
||||||
class EltaFulfillmentService implements CarrierFulfillmentInterface, SupportsTracking
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly EltaClient $client,
|
|
||||||
private readonly EltaLabelRenderer $labelRenderer,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function createShipment(Order $order, ShipmentRequest $request): Shipment
|
|
||||||
{
|
|
||||||
$address = $order->shippingAddress;
|
|
||||||
$weight = $request->weight ?? 0.5;
|
|
||||||
|
|
||||||
$params = [
|
|
||||||
'pel_apost_code' => config('elta.apost_code'),
|
|
||||||
'pel_paral_name' => trim("{$address->first_name} {$address->last_name}"),
|
|
||||||
'pel_paral_address' => $address->line_one,
|
|
||||||
'pel_paral_area' => $address->city,
|
|
||||||
'pel_paral_tk' => $address->postcode,
|
|
||||||
'pel_paral_thl_1' => $address->contact_phone,
|
|
||||||
'pel_paral_thl_2' => '',
|
|
||||||
'pel_service' => '',
|
|
||||||
'pel_baros' => number_format($weight, 3, '.', ''),
|
|
||||||
'pel_baros_xyz' => '',
|
|
||||||
'pel_x' => '',
|
|
||||||
'pel_y' => '',
|
|
||||||
'pel_z' => '',
|
|
||||||
'pel_temaxia' => (string) $request->packageCount,
|
|
||||||
'pel_paral_sxolia' => '',
|
|
||||||
'pel_sur_1' => '',
|
|
||||||
'pel_sur_2' => '',
|
|
||||||
'pel_sur_3' => '',
|
|
||||||
'pel_ant_poso' => '',
|
|
||||||
'pel_ant_poso1' => '',
|
|
||||||
'pel_ant_poso2' => '',
|
|
||||||
'pel_ant_poso3' => '',
|
|
||||||
'pel_ant_poso4' => '',
|
|
||||||
'pel_ant_date1' => '',
|
|
||||||
'pel_ant_date2' => '',
|
|
||||||
'pel_ant_date3' => '',
|
|
||||||
'pel_ant_date4' => '',
|
|
||||||
'pel_asf_poso' => '',
|
|
||||||
'pel_user' => config('elta.user_code'),
|
|
||||||
'pel_ref_no' => (string) $order->id,
|
|
||||||
'pel_insert_flag' => '',
|
|
||||||
'pel_paral_code' => '',
|
|
||||||
'pel_retur_code' => '',
|
|
||||||
];
|
|
||||||
|
|
||||||
$codAmount = null;
|
|
||||||
|
|
||||||
if ($request->paymentMode === 'cod') {
|
|
||||||
$codAmount = $request->amountToCollect ?? $order->total->decimal;
|
|
||||||
$params['pel_ant_poso'] = number_format($codAmount, 2, '.', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $this->client->createVoucher($params)->throwIfError();
|
|
||||||
|
|
||||||
$voucherNo = (string) $response->data['vg_code'];
|
|
||||||
|
|
||||||
$shipment = Shipment::create([
|
|
||||||
'order_id' => $order->id,
|
|
||||||
'carrier' => 'elta',
|
|
||||||
'tracking_reference' => $voucherNo,
|
|
||||||
'meta' => [
|
|
||||||
'weight' => $weight,
|
|
||||||
'ocr_line' => $response->data['ocr_line'] ?? null,
|
|
||||||
'date_time' => $response->data['date_time'] ?? null,
|
|
||||||
'package_count' => $request->packageCount,
|
|
||||||
'cod_amount' => $codAmount,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
// PELVGNEW's own response already carries every child voucher
|
|
||||||
// number for a multi-piece shipment in vg_child_no — no second
|
|
||||||
// request needed, same as the old CREATEAWB/vg_child shape.
|
|
||||||
foreach (array_filter((array) ($response->data['vg_child_no'] ?? [])) as $childVoucherNo) {
|
|
||||||
Shipment::create([
|
|
||||||
'order_id' => $order->id,
|
|
||||||
'carrier' => 'elta',
|
|
||||||
'tracking_reference' => $childVoucherNo,
|
|
||||||
'parent_reference' => $voucherNo,
|
|
||||||
'meta' => $shipment->meta?->toArray() ?? [],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $shipment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PELB64VG (the old, deprecated operation this used to call) fails
|
|
||||||
* with the same st_flag=3 as CREATEAWB — it's the same legacy family.
|
|
||||||
* There's no *NEW replacement: ELTA's own official client renders
|
|
||||||
* labels entirely locally from a bundled report template, never
|
|
||||||
* fetching one from the server at all (see EltaLabelRenderer's
|
|
||||||
* docblock). This reproduces that local rendering instead.
|
|
||||||
*/
|
|
||||||
public function printLabel(Shipment $shipment): string
|
|
||||||
{
|
|
||||||
$pdf = $this->labelRenderer->render($shipment);
|
|
||||||
|
|
||||||
$shipment->update(['label_printed_at' => now()]);
|
|
||||||
|
|
||||||
return $pdf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlike the old, deprecated operation set (which genuinely has no
|
|
||||||
* delete operation), the *NEW family does support cancellation via
|
|
||||||
* PELVGDEL — but it identifies a voucher by its internal pel_vg_id,
|
|
||||||
* not the AWB/tracking number PELVGNEW returns (vg_code). Getting
|
|
||||||
* pel_vg_id requires first listing the account's vouchers via
|
|
||||||
* PELPARALVGNEW1 and matching the row whose pel_paral_vg equals our
|
|
||||||
* tracking_reference.
|
|
||||||
*/
|
|
||||||
public function cancelShipment(Shipment $shipment): void
|
|
||||||
{
|
|
||||||
$voucherId = $this->findVoucherId($shipment->tracking_reference);
|
|
||||||
|
|
||||||
if ($voucherId === null) {
|
|
||||||
throw new EltaApiException(
|
|
||||||
'Could not find voucher '.$shipment->tracking_reference.' in ELTA\'s voucher list — it may already be too old or cancelled to look up.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->client->deleteVoucher(['pel_vg_id' => $voucherId])->throwIfError();
|
|
||||||
|
|
||||||
$shipment->update(['cancelled_at' => now()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findVoucherId(string $trackingReference): ?string
|
|
||||||
{
|
|
||||||
$inId = '0';
|
|
||||||
|
|
||||||
// PELPARALVGNEW1 paginates via in_id, same cursor pattern the
|
|
||||||
// official client uses — walk pages until the voucher is found
|
|
||||||
// or a page comes back empty. flag_1/flag_2 map to the real
|
|
||||||
// client's "Show All"/"All Users" checkboxes — live-confirmed
|
|
||||||
// both must be '1', otherwise even a voucher created moments ago
|
|
||||||
// is filtered out of the (default, unfiltered-looking) list.
|
|
||||||
for ($page = 0; $page < 50; $page++) {
|
|
||||||
$response = $this->client->listVouchers([
|
|
||||||
'pel_code' => config('elta.apost_code'),
|
|
||||||
'pel_user_code' => config('elta.user_code'),
|
|
||||||
'flag_1' => '1',
|
|
||||||
'flag_2' => '1',
|
|
||||||
'in_id' => $inId,
|
|
||||||
])->throwIfError();
|
|
||||||
|
|
||||||
$rows = array_filter(
|
|
||||||
(array) ($response->data['vg_rec'] ?? []),
|
|
||||||
fn (array $row) => filled($row['pel_vg_id'] ?? null),
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($rows === []) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$match = collect($rows)->first(
|
|
||||||
fn (array $row) => ($row['pel_paral_vg'] ?? null) === $trackingReference,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($match !== null) {
|
|
||||||
return $match['pel_vg_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$inId = (string) end($rows)['pel_vg_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function trackShipment(Shipment $shipment): Collection
|
|
||||||
{
|
|
||||||
$response = $this->client->getTracking([
|
|
||||||
'web_vg' => $shipment->tracking_reference,
|
|
||||||
'pel_code' => config('elta.apost_code'),
|
|
||||||
])->throwIfError();
|
|
||||||
|
|
||||||
$rows = array_filter(
|
|
||||||
(array) ($response->data['web_status'] ?? []),
|
|
||||||
fn (array $row) => filled($row['web_date_time'] ?? null),
|
|
||||||
);
|
|
||||||
|
|
||||||
// No explicit delivered-confirmation field like the old
|
|
||||||
// PELTT01's pod_name — pel_rec.a_rec_date_time is blank until
|
|
||||||
// delivery per the real client's own rendering, used as the
|
|
||||||
// delivered signal here; unconfirmed against a genuinely
|
|
||||||
// delivered parcel yet.
|
|
||||||
$isDelivered = filled(trim($response->data['pel_rec']['a_rec_date_time'] ?? '', ' /:'));
|
|
||||||
|
|
||||||
return collect(array_values($rows))->map(function (array $row, int $index) use ($rows, $isDelivered) {
|
|
||||||
$isLast = $index === count($rows) - 1;
|
|
||||||
$dateTime = $row['web_date_time'] ?? '';
|
|
||||||
|
|
||||||
return new TrackingCheckpoint(
|
|
||||||
status: $isLast && $isDelivered
|
|
||||||
? TrackingStatus::Delivered
|
|
||||||
: $this->guessStatusFromTitle($row['web_status_name'] ?? ''),
|
|
||||||
carrierStatus: $row['web_status_name'] ?? null,
|
|
||||||
message: $row['web_sxolia'] ?: ($row['web_status_name'] ?? null),
|
|
||||||
location: $row['web_station'] ?? null,
|
|
||||||
occurredAt: Carbon::createFromFormat('YmdHi', substr($dateTime, 0, 12)),
|
|
||||||
meta: $row,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* web_status_name is free text with no structured status code — same
|
|
||||||
* limitation Acs\AcsFulfillmentService::guessStatusFromAction() has.
|
|
||||||
* Live-confirmed text seen so far: "ΔΗΜΙΟΥΡΓΙΑ ΣΥ.ΔΕ.ΤΑ. ΑΠΟ ΠΕΛΑΤΗ"
|
|
||||||
* (voucher created) — matched via a Pending-ish fallback below since
|
|
||||||
* it isn't yet in transit. Other statuses are best-guess substring
|
|
||||||
* matches to refine as more real tracking text is observed.
|
|
||||||
*/
|
|
||||||
private function guessStatusFromTitle(string $title): TrackingStatus
|
|
||||||
{
|
|
||||||
$title = mb_strtolower($title);
|
|
||||||
|
|
||||||
return match (true) {
|
|
||||||
str_contains($title, 'παραδόθηκε') => TrackingStatus::Delivered,
|
|
||||||
str_contains($title, 'διανομή') => TrackingStatus::OutForDelivery,
|
|
||||||
str_contains($title, 'παραλαβή') => TrackingStatus::CollectedFromSender,
|
|
||||||
str_contains($title, 'μεταφορά') || str_contains($title, 'διαμετακόμιση') => TrackingStatus::InTransit,
|
|
||||||
default => TrackingStatus::Pending,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
use Barryvdh\DomPDF\Facade\Pdf;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Modules\Core\Shipping\Models\Shipment;
|
|
||||||
use Picqer\Barcode\BarcodeGeneratorPNG;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders an ELTA shipping label as a PDF ourselves — there is no *NEW
|
|
||||||
* operation for fetching one (see EltaFulfillmentService::printLabel()'s
|
|
||||||
* docblock), so this reproduces ELTA's own official label design instead
|
|
||||||
* of a server-fetched file.
|
|
||||||
*
|
|
||||||
* Field layout/text is traced directly from ELTA's own 2024 Customer
|
|
||||||
* Client Installation Manual, which includes real sample images of both
|
|
||||||
* the A4 voucher (3 stacked copies: delivery copy, sender's copy, a
|
|
||||||
* ΤΑΧΥΠΛΗΡΩΜΗ payment-receipt stub) and the A6 thermal label (single
|
|
||||||
* copy, with a return-reason checkbox list the A4 version doesn't have)
|
|
||||||
* — not from decompiled client code, which an earlier version of this
|
|
||||||
* class was built from and turned out to look nothing like a real
|
|
||||||
* label. The barcode is Code 128 (single barcode per label, matching
|
|
||||||
* both real samples) encoding the voucher number itself — an earlier
|
|
||||||
* version guessed Code 39 with a second "OCR" barcode from misreading
|
|
||||||
* decompiled code; neither survived comparison against the real
|
|
||||||
* samples.
|
|
||||||
*/
|
|
||||||
class EltaLabelRenderer
|
|
||||||
{
|
|
||||||
public function __construct(private readonly AreaResolver $areaResolver) {}
|
|
||||||
|
|
||||||
public function render(Shipment $shipment): string
|
|
||||||
{
|
|
||||||
$order = $shipment->order;
|
|
||||||
$address = $order->shippingAddress;
|
|
||||||
$meta = $shipment->meta?->toArray() ?? [];
|
|
||||||
|
|
||||||
$destinationStation = $this->areaResolver->resolve($address->postcode);
|
|
||||||
|
|
||||||
$dateTime = (string) ($meta['date_time'] ?? '');
|
|
||||||
$occurredAt = strlen($dateTime) >= 12
|
|
||||||
? Carbon::createFromFormat('YmdHi', substr($dateTime, 0, 12))
|
|
||||||
: now();
|
|
||||||
|
|
||||||
$voucherNo = $shipment->tracking_reference;
|
|
||||||
$ocrLine = (string) ($meta['ocr_line'] ?? '');
|
|
||||||
$packageCount = (int) ($meta['package_count'] ?? 1);
|
|
||||||
|
|
||||||
$generator = new BarcodeGeneratorPNG();
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'sender_name' => config('elta.sender_name'),
|
|
||||||
'sender_address' => config('elta.sender_address'),
|
|
||||||
'sender_postcode' => config('elta.sender_postcode'),
|
|
||||||
'sender_area' => config('elta.sender_area'),
|
|
||||||
'sender_phone' => config('elta.sender_phone'),
|
|
||||||
'apost_code' => config('elta.apost_code'),
|
|
||||||
|
|
||||||
'recipient_name' => trim("{$address->first_name} {$address->last_name}"),
|
|
||||||
'recipient_address' => $address->line_one,
|
|
||||||
'recipient_postcode' => $address->postcode,
|
|
||||||
'recipient_area' => $address->city,
|
|
||||||
'recipient_phone' => $address->contact_phone,
|
|
||||||
|
|
||||||
// Mirrors Sydeta.cs::print_vg()'s own dataRow["sender_1..5"] /
|
|
||||||
// dataRow["rec_1..5"] construction: name on its own line, then
|
|
||||||
// address, then "TK:<postcode> <area> ΤΗΛ:<phone>" — each of
|
|
||||||
// the RDLC's rectangle8/rectangle9 boxes renders these as 3
|
|
||||||
// separate textbox lines (not 5; sender_2/sender_3 and
|
|
||||||
// rec_2/rec_3 only appear when a line's text exceeds the
|
|
||||||
// report's 40-char wrap width, which our own data never hits
|
|
||||||
// in practice for a name/address/contact triple).
|
|
||||||
'sender_lines' => [
|
|
||||||
config('elta.sender_name'),
|
|
||||||
config('elta.sender_address'),
|
|
||||||
'TK:'.config('elta.sender_postcode').' '.config('elta.sender_area').' ΤΗΛ:'.config('elta.sender_phone'),
|
|
||||||
],
|
|
||||||
'recipient_lines' => [
|
|
||||||
trim("{$address->first_name} {$address->last_name}"),
|
|
||||||
$address->line_one,
|
|
||||||
'TK:'.$address->postcode.' '.$address->city,
|
|
||||||
'ΤΗΛ: '.$address->contact_phone,
|
|
||||||
],
|
|
||||||
|
|
||||||
'date' => $occurredAt->format('d/m/Y'),
|
|
||||||
'time' => $occurredAt->format('H:i'),
|
|
||||||
|
|
||||||
'voucher_no' => $voucherNo,
|
|
||||||
'barcode_voucher' => $this->barcodeDataUri($generator, $voucherNo),
|
|
||||||
|
|
||||||
'weight' => $meta['weight'] ?? null,
|
|
||||||
'package_count' => $packageCount,
|
|
||||||
'package_label' => $packageCount > 1
|
|
||||||
? sprintf('001/%03d', $packageCount)
|
|
||||||
: '001',
|
|
||||||
'multiPiece' => $packageCount > 1,
|
|
||||||
|
|
||||||
'station_apo' => config('elta.origin_station_code'),
|
|
||||||
'station_pros' => $destinationStation->code,
|
|
||||||
'station_pros_title' => $destinationStation->name,
|
|
||||||
|
|
||||||
'service_code' => '214',
|
|
||||||
'service_name' => 'ΠΟΛΗ ΠΟΛΗ -ΠΘ- ΝΗΣΙ',
|
|
||||||
|
|
||||||
'xreosi' => 'ΧΡΕΩΣΗ ΑΠΟΣΤΟΛΕΑ ΠΙΣΤΩΣΗ',
|
|
||||||
'siimvasi' => '131775-9',
|
|
||||||
|
|
||||||
'cod_amount' => $meta['cod_amount'] ?? null,
|
|
||||||
'ocr_line' => $ocrLine,
|
|
||||||
// The A6 layout's 3-cell row shows a 13-digit reference derived
|
|
||||||
// from the OCR line, not the barcode's own voucher text —
|
|
||||||
// confirmed against the real sample: ">14260009814363<..."
|
|
||||||
// yields reference "1426000981436" (13 chars after the '>').
|
|
||||||
'ocr_reference' => strlen($ocrLine) >= 14 ? substr($ocrLine, 1, 13) : '',
|
|
||||||
|
|
||||||
'order_reference' => (string) $order->id,
|
|
||||||
|
|
||||||
// sydetaE.rdlc's "Πρόσθετες Υπηρεσίες" (sur_1..4) and the
|
|
||||||
// ΑΝΤΙΚΑΤΑΒΟΛΗ banner (antik_1) are only populated by ELTA's
|
|
||||||
// own client when the shipper ticks special-service checkboxes
|
|
||||||
// (Sydeta.cs::print_vg()'s checkBox2-5) or a COD amount is set
|
|
||||||
// — neither is tracked on our Shipment model today, so these
|
|
||||||
// render as empty boxes, matching what a plain "credit" send
|
|
||||||
// with no extras or COD looks like on a real label.
|
|
||||||
'sur_1' => null,
|
|
||||||
'sur_2' => null,
|
|
||||||
'sur_3' => null,
|
|
||||||
'sur_4' => null,
|
|
||||||
'antik_1' => $meta['cod_amount'] ?? null ? '* ΠΡΟΣΟΧΗ ΑΝΤΙΚΑΤΑΒΟΛΗ *' : null,
|
|
||||||
'volumetric_weight' => $meta['volumetric_weight'] ?? null,
|
|
||||||
|
|
||||||
'eltaLogo' => $this->logoDataUri(),
|
|
||||||
];
|
|
||||||
|
|
||||||
$paperSize = config('elta.label_paper_size', 'a4');
|
|
||||||
$view = $paperSize === 'a6'
|
|
||||||
? 'core::shipping.carriers.elta.label-a6'
|
|
||||||
: 'core::shipping.carriers.elta.label-a4';
|
|
||||||
|
|
||||||
$pdf = Pdf::loadView($view, $data);
|
|
||||||
|
|
||||||
// A6 = 104mm x 148mm, per RDLCPrinter.cs's own DeviceInfo
|
|
||||||
// override for printer_size==2 (1mm ≈ 2.8346pt) — note this is
|
|
||||||
// ELTA's own thermal-label size, not the ISO A6 (105x148mm) the
|
|
||||||
// old comment here named. A 4-value paper array plus an
|
|
||||||
// orientation string together confuse dompdf into doubling the
|
|
||||||
// canvas and silently overflowing to a second blank page
|
|
||||||
// (reproduced with a bare empty page); the array alone is
|
|
||||||
// already portrait (height > width), so the orientation
|
|
||||||
// argument is both redundant and the actual bug.
|
|
||||||
$paperSize === 'a6'
|
|
||||||
? $pdf->setPaper([0, 0, 294.80, 419.53])
|
|
||||||
: $pdf->setPaper('a4');
|
|
||||||
|
|
||||||
return $pdf->output();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PNG over SVG — dompdf's inline <svg> support doesn't reliably
|
|
||||||
* render the barcode generator's <rect>-based bars (confirmed: the
|
|
||||||
* SVG output rendered as literal text in a live test), while a
|
|
||||||
* base64 PNG <img> is unambiguous.
|
|
||||||
*/
|
|
||||||
private function barcodeDataUri(BarcodeGeneratorPNG $generator, string $payload): string
|
|
||||||
{
|
|
||||||
$png = $generator->getBarcode($payload, BarcodeGeneratorPNG::TYPE_CODE_128);
|
|
||||||
|
|
||||||
return 'data:image/png;base64,'.base64_encode($png);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Embedded as a base64 data URI rather than referenced by URL —
|
|
||||||
* resources/logos/ is only copied into a consuming app's public/ on
|
|
||||||
* vendor:publish, which isn't guaranteed to have run, and dompdf
|
|
||||||
* needs either a real filesystem path or a working absolute URL.
|
|
||||||
*/
|
|
||||||
private function logoDataUri(): string
|
|
||||||
{
|
|
||||||
$path = __DIR__.'/../../../../resources/logos/elta-courier-logo.png';
|
|
||||||
|
|
||||||
return 'data:image/png;base64,'.base64_encode(file_get_contents($path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
use Lunar\DataTypes\ShippingOption;
|
|
||||||
use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest;
|
|
||||||
use Lunar\Shipping\Interfaces\ShippingRateInterface;
|
|
||||||
use Lunar\Shipping\Models\ShippingRate;
|
|
||||||
use Modules\Core\Shipping\Concerns\ExcludesRestrictedProducts;
|
|
||||||
use Modules\Core\Shipping\Concerns\ResolvesFixedPricing;
|
|
||||||
use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
|
|
||||||
use Modules\Core\Shipping\Contracts\SupportsCashCollection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* None of ELTA's four SOAP operations (create/label/track/station) expose
|
|
||||||
* a price-quote/calculation call, so — like Box Now — this always
|
|
||||||
* resolves the method's own charge_by + price-break configuration, never
|
|
||||||
* live pricing. Does not implement SupportsLivePricing.
|
|
||||||
*
|
|
||||||
* Implements SupportsCashCollection: an ELTA courier hands a parcel to
|
|
||||||
* the recipient in person, the same fact as AcsRateDriver, unlike Box
|
|
||||||
* Now's unattended lockers.
|
|
||||||
*/
|
|
||||||
class EltaRateDriver implements ShippingRateInterface, DeclaresFulfillmentType, SupportsCashCollection
|
|
||||||
{
|
|
||||||
use ResolvesFixedPricing;
|
|
||||||
use ExcludesRestrictedProducts;
|
|
||||||
|
|
||||||
public ShippingRate $shippingRate;
|
|
||||||
|
|
||||||
public function name(): string
|
|
||||||
{
|
|
||||||
return 'ELTA Courier';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fulfillmentType(): string
|
|
||||||
{
|
|
||||||
return 'carrier';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function collectsCash(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function description(): string
|
|
||||||
{
|
|
||||||
return 'Delivery via ELTA Courier.';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resolve(ShippingOptionRequest $shippingOptionRequest): ?ShippingOption
|
|
||||||
{
|
|
||||||
if ($this->cartHasExcludedProducts($shippingOptionRequest->shippingRate, $shippingOptionRequest->cart)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->resolveFixedPrice(
|
|
||||||
$shippingOptionRequest->shippingRate,
|
|
||||||
$shippingOptionRequest->shippingRate->shippingMethod,
|
|
||||||
$shippingOptionRequest->cart,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function on(ShippingRate $shippingRate): self
|
|
||||||
{
|
|
||||||
$this->shippingRate = $shippingRate;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
use Modules\Core\Shipping\Carriers\Elta\Exceptions\EltaApiException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a SoapClient::READ() stdClass result — every ELTA operation's
|
|
||||||
* response carries the same st_flag (0=success, 1-99=error)/st_title
|
|
||||||
* shape, live-verified against the real production endpoint (see
|
|
||||||
* EltaClient's own docblock). Mirrors Acs\AcsResponse's hasError/data
|
|
||||||
* shape so the rest of the integration (rate driver, fulfillment service)
|
|
||||||
* reads identically regardless of transport (SOAP here vs. ACS's JSON).
|
|
||||||
*/
|
|
||||||
class EltaResponse
|
|
||||||
{
|
|
||||||
private function __construct(
|
|
||||||
public readonly bool $hasError,
|
|
||||||
public readonly ?string $errorMessage,
|
|
||||||
public readonly array $data,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function fromSoapResult(mixed $result): self
|
|
||||||
{
|
|
||||||
$data = json_decode(json_encode($result), true) ?? [];
|
|
||||||
$flag = (int) ($data['st_flag'] ?? 0);
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
hasError: $flag !== 0,
|
|
||||||
errorMessage: $flag !== 0 ? ($data['st_title'] ?? 'Unknown ELTA error') : null,
|
|
||||||
data: $data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function throwIfError(): self
|
|
||||||
{
|
|
||||||
if ($this->hasError) {
|
|
||||||
throw new EltaApiException($this->errorMessage ?? 'Unknown ELTA API error', $this->data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
||||||
|
|
||||||
class EltaStation
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly string $code,
|
|
||||||
public readonly string $name,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Elta\Exceptions;
|
|
||||||
|
|
||||||
use RuntimeException;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class EltaApiException extends RuntimeException
|
|
||||||
{
|
|
||||||
public function __construct(string $message, public readonly array $data = [], ?Throwable $previous = null)
|
|
||||||
{
|
|
||||||
parent::__construct($message, previous: $previous);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user