Appearance
VAT Rate Management
Flow ID: AD-50 | Module(s): eshop, Order/Vat domain | Complexity: Medium Last Updated: 2026-04-28
Business Context
VAT rate management provides CRUD operations for the shop's tax rate catalog. Each product row stores a foreign-key-less vat_id referencing a row in shop_product_vats; that row's value is multiplied into list prices, cart totals, and captured verbatim into every order line at checkout. Greece typically uses 24% (standard), 13% (reduced), 6.5/6% (super-reduced) and 0% (exempt), which matches the four canonical IDs baked into the bridge table migration (1=>0%, 2=>13%, 3=>24%, 4=>6%). The default seed only ships 24.0 and 13.0.
Two parallel CRUD surfaces exist:
- The legacy admin page at
/eshop/vats_admin.htm, backed by a tinyAdv_vats_admincontroller with form-validation-lite and a single delete guard. - The modern REST API under
/rest/order/vat, backed bysrc/Domains/Order/Vat/andsrc/Rest/Order/Vat, which is missing some protections the legacy path enforces (no cache invalidation). Validator rules for create/update were added in GH #7; RBAC for write endpoints was added in GH #10; the delete guard was added in commit7049a4555. All three now match or exceed legacy behaviour.
VAT rates flow through a lazy singleton (AdvVatForOrder) that is a pass-through by default — nothing manipulates rates per-country out of the box. The shop_prices_view database view joins shop_product_vats directly on INNER JOIN, so any orphaned product silently disappears from listings. Historical VAT is preserved per order line in shop_order_basket.product_vat, so rate changes never mutate past orders.
API Reference
REST Endpoints (Modern Layer)
| Method | Path | Action | Auth |
|---|---|---|---|
GET | /rest/order/vat | List VAT rates (paginated, filterable) | Guest |
GET | /rest/order/vat/{id} | Get single VAT rate by ID | Guest |
GET | /rest/order/vat/item | Get single VAT rate by filter | Guest |
POST | /rest/order/vat | Create new VAT rate | JWT — AUTH_ROLE_ADMIN or AUTH_ROLE_PRODUCTS |
POST | /rest/order/vat/{id} | Update existing VAT rate | JWT — AUTH_ROLE_ADMIN or AUTH_ROLE_PRODUCTS |
DELETE | /rest/order/vat/{id} | Delete VAT rate | JWT — AUTH_ROLE_ADMIN or AUTH_ROLE_PRODUCTS |
Routes registered in application/config/rest_routes.php lines 701-706 (read) and 1349-1354 (write), each with a locale-prefixed variant. Write endpoints require AUTH_ROLE_ADMIN or AUTH_ROLE_PRODUCTS via application/config/rest_policies.php (GH #10); read endpoints are public (guest).
Legacy Admin Routes
| Route | Controller | Method | HTTP | Description |
|---|---|---|---|---|
eshop/vats_admin | Adv_vats_admin | index() | GET | List all VAT rates |
eshop/vats_admin/add | Adv_vats_admin | add() | GET/POST | Create VAT rate |
eshop/vats_admin/edit/{id} | Adv_vats_admin | edit($id) | GET/POST | Edit VAT rate |
eshop/vats_admin/delete/{id} | Adv_vats_admin | delete($id) | GET | Delete VAT rate |
Defined in application/config/routes.php:328-331 (default + locale-prefixed).
Required roles (controller constructor, ecommercen/eshop/controllers/Adv_vats_admin.php:22-28): AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS.
Admin menu roles (application/config/admin_menu.php:519-525): AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN — the menu omits AUTH_ROLE_PRODUCTS, so a PRODUCTS-role user cannot see the link but can still reach the URL directly (security-through-obscurity mismatch).
Code Flow
Legacy Admin CRUD
Adv_vats_admin::add() / edit($id)
|
+--> POST submit -> validation()
| \-- set_rules('vat_value', ..., 'trim|numeric')
| (no required, no range, no uniqueness)
|
+--> Build data array ['value' => $this->input->post('vat_value')]
+--> $this->vats_model->addRecord($data) or editRecord($id, $data)
+--> afterAdd($id) | afterEdit($id) [empty stub in base]
+--> redirect('eshop/vats_admin')Adv_vats_admin::delete($id)
|
+--> $this->vats_model->deleteRecord($id)
| |
| +--> canDelete($id) -> SELECT count(*) FROM shop_product WHERE vat_id = ?
| +--> if zero references: DELETE FROM shop_product_vats WHERE id = ?
| \-- else: return false (silent)
|
+--> afterDelete($id) [empty stub in base]
+--> redirectThe afterAdd() / afterEdit() / afterDelete() hooks at ecommercen/eshop/controllers/Adv_vats_admin.php:134-147 are empty stubs, intended to be overridden in client application/modules/eshop/controllers/Vats_admin.php. The base does not clear pscache, write an audit trail, or re-parse product prices — stale cached rates persist until TTL expiry.
Modern REST Write Flow
POST /rest/order/vat or POST /rest/order/vat/{id}
|
+--> Rest\Order\Controllers\Vat::store() / update($id)
+--> HandlesWriteActions::doStore() / doUpdate($id)
| |
| +--> WriteData::from(request()) -- single ?float $value field
| +--> Validator::validateForCreate($data) / validateForUpdate($data, $id)
| | validateForCreate: value required; range [0,100]; max 1 decimal place;
| | | no duplicate via WriteRepository::existsWithValue($value)
| | validateForUpdate: value optional; if present, same range + precision;
| | duplicate check excludes current row:
| | WriteRepository::existsWithValue($value, (int)$id)
| +--> WriteService::create() / update($id)
| \-- WriteRepository->insert/update into shop_product_vats
|
+--> 200 + VatResource::toArray() => ['id' => int, 'value' => (float)$value]DELETE /rest/order/vat/{id}
|
+--> Rest\Order\Controllers\Vat::destroy($id)
+--> HandlesWriteActions::doDestroy($id)
| |
| +--> service->get($id) [existence check]
| | \-- 404 if not found
| |
| +--> WriteService::delete($id)
| | |
| | +--> Validator::validateForDelete($id)
| | | \-- WriteRepository::hasReferences($id)
| | | SELECT 1 FROM shop_product WHERE vat_id = ? LIMIT 1
| | | if any row: throw ValidationException ['id' => 'Cannot delete VAT rate: referenced by existing products']
| | |
| | \-- WriteRepository::delete($id) [only reached if no references]
| |
| +--> 200 {'success': true, 'message': 'Entity deleted successfully'}
| \-- on ValidationException: 409 {'success': false, 'errors': {'id': '...'}}src/Domains/Order/Vat/WriteService.php:47-51 (7049a4555):
php
public function delete(int|string $id): bool
{
$this->validator->validateForDelete($id);
return $this->writeRepository->delete($id);
}The guard calls Validator::validateForDelete() which delegates to WriteRepository::hasReferences() (src/Domains/Order/Vat/Repository/WriteRepository.php). If any product references the VAT, ValidationException is thrown and caught by HandlesWriteActions::doDestroy() (src/Rest/Support/Controllers/HandlesWriteActions.php) as HTTP 409 Conflict. This matches the legacy canDelete() behaviour.
Data Model
shop_product_vats Table
Defined exclusively in database/initial/initial.sql:1877-1881. No Phinx migration has ever altered it.
| Column | Type | Nullable | Description |
|---|---|---|---|
id | INT(11) AUTO_INCREMENT PK | No | Surrogate key |
value | DECIMAL(11,1) NOT NULL | No | VAT rate percentage |
Constraints / notes:
- No foreign key from
shop_product.vat_id→shop_product_vats.id(the relationship is purely logical). - No
UNIQUEconstraint onvalue— duplicate rates are permitted. - No
description,label,country, oractiveflag. - No
created_at/updated_attimestamps. DECIMAL(11,1)allows one decimal digit only. You can store24.0,13.0,6.5; you cannot store23.99(silently truncated to24.0).- Default seed (
database/seeders/data/global/shop_product_vats.json) inserts only24.0and13.0. - Canonical ID convention from
database/migrations/20240923141428_new_bridge_tables.php:46comment:1=>0%, 2=>13%, 3=>24%, 4=>6%.
Tables that Reference VAT
| Table | Column | Type | Purpose |
|---|---|---|---|
shop_product | vat_id INT NOT NULL | FK-less reference | Product's assigned VAT rate (initial.sql:1524,1560) |
shop_order | total_vat DECIMAL(11,2) NULL | Order-level VAT amount in currency | Aggregate captured at checkout |
shop_order_basket | product_vat DECIMAL(11,2) NOT NULL | Per-line snapshot of VAT rate | Historical preservation — subsequent rate changes do not alter past orders |
coupons | old_total_vat DECIMAL(11,2) NOT NULL DEFAULT 0.00 | Audit/rollback | Coupon side of VAT audit trail |
Unrelated: skroutz_invoice_details.vat_number etc. is a customer ΑΦΜ/VIES tax identifier, not a reference to shop_product_vats.
shop_prices_view (only DB view in the system)
Defined at database/initial/initial.sql:2451-2467. Joins shop_product → shop_product_vats via INNER JOIN and computes original_price / final_price with CASE branches for VAT-inclusive and discount variants.
Fragility: The INNER JOIN means any product whose vat_id row is deleted from shop_product_vats silently disappears from every query that goes through the view — including storefront listings. The view also uses the raw shop_product_vats.value directly and does not route through AdvVatForOrder, so it always returns Greek-mainland inclusive pricing regardless of country (consistent with the default enableOrderVatManipulation=false but diverges if the flag is ever flipped).
Domain Layer
Modern Domain (src/Domains/Order/Vat/)
Note the context: Order, not Product — even though functionally VAT is a product taxonomy, the domain is filed under the Order bounded context.
| File | Responsibility |
|---|---|
src/Domains/Order/Vat/Repository/Entity.php | @property int $id, @property float $value; no TABLE constant |
src/Domains/Order/Vat/Repository/Repository.php | Read repo, protected string $table = 'shop_product_vats'; |
src/Domains/Order/Vat/Repository/RepositoryConfigurator.php | getRelations(): array { return []; } — no joins |
src/Domains/Order/Vat/Repository/WriteRepository.php | Write repo, extends BaseWriteRepository, PK id; adds hasReferences(int $id): bool and existsWithValue(float $value, ?int $excludeId = null): bool |
src/Domains/Order/Vat/Service.php | Read service: all(), item(), get(); implements ReadService, RendersPagination |
src/Domains/Order/Vat/WriteService.php | create(), update(int|string $id, array $data), delete(int|string $id) — each calls validator before repository. update() passes $id through to validateForUpdate() so the duplicate check can exclude the record being edited (GH #7). delete() calls validateForDelete($id) (added in 7049a4555). |
src/Domains/Order/Vat/WriteData.php | DTO with single public readonly ?float $value; OpenAPI schema name Vat, required ['value'] |
src/Domains/Order/Vat/Validator.php | validateForCreate(WriteData $data): requires non-null value, enforces range [0, 100], rejects more than 1 decimal place, blocks duplicate via existsWithValue(). validateForUpdate(WriteData $data, int|string|null $id): same checks when value is present, duplicate check excludes the current record. validateForDelete(int|string $id) (added in 7049a4555): rejects deletion if hasReferences() returns true. (GH #7) |
src/Domains/Order/Vat/ListRequest.php | Allowed filters: id, value (Exact). Allowed sorts: id, value. No relation sorts. |
REST Layer (src/Rest/Order/)
| File | Responsibility |
|---|---|
src/Rest/Order/Controllers/Vat.php | 148 lines; extends HandlesRestfulActions + HandlesWriteActions; full OpenAPI annotations; tags Rest/Order/Vat; schemas Vat, VatResource, VatCollection |
src/Rest/Order/Resources/Vat/Resource.php | ['id' => int, 'value' => (float)$resource->value] |
src/Rest/Order/Resources/Vat/Collection.php | Collects Resource::class |
Constructor (src/Rest/Order/Controllers/Vat.php:27-36) injects: ReadService, $resourceClass, $collectionClass, $listRequestClass, WriteService.
DI Registration
src/Domains/Order/container.php:18-24— Domain servicessrc/Rest/Order/container.php:22-27— Controller + resources
Legacy Layer (ecommercen/)
| File | Lines | Responsibility |
|---|---|---|
ecommercen/eshop/controllers/Adv_vats_admin.php | 148 | Admin CRUD controller, extends Admin_c |
application/modules/eshop/controllers/Vats_admin.php | — | Empty client subclass override point |
ecommercen/eshop/models/Adv_vats_model.php | 152 | Legacy model, extends Adv_base_model |
application/modules/eshop/models/Vats_model.php | — | Empty client subclass override point |
application/views/admin/vats/list.php | — | Listing view |
application/views/admin/vats/create.php | — | Create form |
application/views/admin/vats/update.php | — | Edit form |
Notable model methods:
deleteRecord($vatId)(ecommercen/eshop/models/Adv_vats_model.php:129-139): callscanDelete()and only deletes if it returns true.canDelete($vatID)(lines 147-151):SELECT count(*) FROM shop_product WHERE vat_id = ?— returns true only if zero rows. Only checksshop_product; does not checkshop_order_basket(different column anyway) or soft-deleted products.
VAT Calculation Pipeline
AdvVatForOrder Singleton
Defined in ecommercen/eshop/libraries/AdvVatForOrder.php (76 lines). Empty client subclass override at application/modules/eshop/libraries/VatForOrder.php.
Constructor reads config_item('enableOrderVatManipulation') from application/config/app.php:524, which defaults to false. When the flag is false — which is the platform-default state — AdvVatForOrder is a pure pass-through: whatever rate is stored in shop_product_vats is returned verbatim by vat().
Properties:
php
bool $enableVatCompute;
?OrderChargeAreaType $chargeAreaType;
?OrderDeliverAreaType $deliverAreaType;
bool $isInvoice;Area Enums
OrderDeliverAreaType: Greece=1, GreeceReduced=2, EuropeUnion=3, NotEuropeUnion=4
OrderChargeAreaType: Greece=1, GreeceReduced=2, EuropeVIES=3, EuropeNoVIES=4, ThirdCountries=5
setDeliverAreaType($country, $greekArea = '') (lines 25-37)
if ($country == 'GR' && in_array($greekArea, ['not yet implemented', 'reduced invoice area']))
-> GreeceReduced
elseif ($country == 'GR')
-> Greece
elseif (isEuCountry($country))
-> EuropeUnion
else
-> NotEuropeUnionvat(float $vatValue, bool $isDigital = false)
if ($enableVatCompute === false) return $vatValue; // default path
return $isInvoice ? $this->invoiceVat() : $this->receiptVat();invoiceVat()— marked//TODO: not yet implemented; returns$vatValueunchangedreceiptVat()— returns0if$deliverAreaType === NotEuropeUnion, else$vatValue$isDigitalflag is set by callers but never consumed byvat()/invoiceVat()/receiptVat()$chargeAreaTypeproperty is set bysetChargeAreaType()(which does branch on$isVIESto pickEuropeVIESvsEuropeNoVIES) but the property is never read anywhere —vat(),invoiceVat(), andreceiptVat()only consult$deliverAreaType
Dead Code Finding
No production code path passes a non-empty $greekArea argument to setDeliverAreaType(). A codebase-wide search shows only tests/Legacy/Eshop/AdvVatForOrderTest.php supplies 'reduced invoice area' or 'not yet implemented'. The Greek-islands reduced-VAT branch is unreachable from production. Combined with the empty invoiceVat() stub and the ignored $isDigital/$chargeAreaType/$isVIES inputs, a significant portion of AdvVatForOrder is cosmetic.
How VAT Reaches a Product Price
Central entry at ecommercen/eshop/models/Adv_product_parser_model.php:112-127:
php
$vatForOrder = VatForOrder::getInstance();
$productVats = getResultObjectAsIndexedArray(
$this->pscache->model('vats_model', 'getRecords', [], $this->pscacheTtl)
);
foreach ($this->products as &$product) {
$product->vat_value = $vatForOrder->vat($productVats[$product->vat_id]->value);
...
$this->setPrices($product, $vatForOrder);
}setPrices() (lines 464-479) applies VAT via applyVatWithoutFormat() in ecommercen/helpers/shopmodule_helper.php:269-272:
php
return round(($amount + (($vat / 100) * $amount)) * $qty, 2);A second cached site exists at ecommercen/eshop/models/Adv_new_product_prices_model.php:67.
Capturing VAT into Orders
At ecommercen/eshop/models/Adv_order_model.php:478-489:
php
$cart[$rowId]['vat_rate_captured'] = $vatForOrder->vat($prItem->vat_value);Persisted via ecommercen/helpers/eshop_helper.php:102-125 into shop_order_basket.product_vat (DECIMAL(11,2)). This is the historical preservation mechanism — once an order is placed, subsequent changes to shop_product_vats rows do not retroactively alter past order lines.
Configuration
Feature Flag: enableOrderVatManipulation
- File:
application/config/app.php:524 - Default:
false - When false (default):
AdvVatForOrder::vat()returns the rawshop_product_vats.valueunchanged. No country logic, no invoice branching, no digital-goods branching. - When true: control enters
invoiceVat()/receiptVat(), butinvoiceVat()is a//TODO: not yet implementedpass-through andreceiptVat()only zeroes VAT forNotEuropeUniondeliveries. Setting it totruewithout additional work changes very little behavior.
Hard-coded Marketplace Shipping VAT
application/config/app.php:529 defines 'shipping_vat' => 24 inside publicMarketplaceSettings. This constant is totally independent of shop_product_vats. Public Marketplace feed generators read it directly. Admin changes to shop_product_vats do not propagate here.
Cache Behavior
ecommercen/eshop/models/Adv_product_parser_model.php:113-115— cachesvats_model.getRecords()viapscachewith$this->pscacheTtl.ecommercen/eshop/models/Adv_new_product_prices_model.php:67— second cached site.ecommercen/helpers/pscache_helper.php:66-82— declaresvats_modelin theproductsclearCachegroup.- BUT:
Adv_vats_adminnever invokesclearCache('products'). The legacy admin writes toshop_product_vatsand relies on the TTL to expire. During that TTL window, price listings still reflect the old rate.
Hidden Writers
Pharmacy ERP Side-Channel (Europharmacy)
ecommercen/helpers/farmakon_helper.php:100-122 — during Europharmacy ERP product sync, new VAT rows are auto-created when an inbound rate is not yet present:
php
$vatId = array_search($productVat, $vats);
if ($vatId === false) {
$ci->db->insert('shop_product_vats', ['value' => $productVat]);
$vatId = $ci->db->insert_id();
$vats[$vatId] = $productVat;
}- Bypasses the modern Validator entirely — now that the Validator enforces required value, range, and duplicate checks (GH #7), this ERP side-channel is a live gap: new rates inserted by Europharmacy skip all those guards.
- Strict-string match via
strval()means typos (e.g.'24.00'vs'24.0') silently create duplicate rows. - No
afterAddhook, no cache clear.
Pharmacy Accounting String Match
ecommercen/helpers/farmakon_helper.php:283-318 — hard-coded string branches on '6.00', '13.00', and '24.00' for pharmacy accounting. Any rate other than those three (e.g. Greek-island reduced '17.00') silently fails to match and is skipped by this code path.
Client Extension Points
- Override legacy admin controller: create
application/modules/eshop/controllers/Vats_admin.php(empty client subclass scaffold already exists). - Override legacy model: create
application/modules/eshop/models/Vats_model.php. - Override
AdvVatForOrder: createapplication/modules/eshop/libraries/VatForOrder.php(empty client subclass scaffold already exists) and implementinvoiceVat()/receiptVat()/ the dead branches as needed. - REST extensions: in client repos, extend
Custom\Domains\Order\Vat\*and register DI aliases undercustom/Domains/container.php. - Hook into legacy CRUD: override
afterAdd($id),afterEdit($id),afterDelete($id)in the clientVats_admincontroller to invalidate caches, emit audit events, or recompute prices.
Business Rules
- Historical preservation: Orders store per-line
shop_order_basket.product_vatat checkout, so rate changes never affect past orders. (ecommercen/helpers/eshop_helper.php:102-125) - Legacy delete guard blocks deletion of a VAT rate while any row in
shop_productreferences it. (ecommercen/eshop/models/Adv_vats_model.php:147-151) - Greek compliance: VAT rates must align with Greek tax law tiers for proper invoicing. The canonical ID convention is
1=>0%, 2=>13%, 3=>24%, 4=>6%. (database/migrations/20240923141428_new_bridge_tables.php:46) - Decimal precision is one digit:
DECIMAL(11,1)forbids rates like23.99. Half-percent rates such as6.5are valid. - Cache staleness window: Legacy admin changes do not invalidate pscache; stale rates persist until TTL expiry. (
ecommercen/helpers/pscache_helper.php:66-82) - Pass-through VAT by default:
enableOrderVatManipulation=falsemeansAdvVatForOrderreturns raw stored rates; country/area logic is dormant. (application/config/app.php:524) - Marketplace shipping VAT is hard-coded: changes to
shop_product_vatsdo not affect marketplace shipping VAT. (application/config/app.php:529)
Known Issues & Security Gaps
Empty— Fixed (GH #7).Validatoron create/updatesrc/Domains/Order/Vat/Validator.phpnow rejects null/missingvalueon create, enforces the range[0, 100], pins decimal precision to a single digit to match theDECIMAL(11,1)column, and blocks duplicate rates viaWriteRepository::existsWithValue().- Legacy form validation is too loose —
ecommercen/eshop/controllers/Adv_vats_admin.php:129-132uses onlytrim|numeric; there is norequired, no range check, no uniqueness. Submitting an empty POST silently inserts/updates to0. - RBAC mismatch — controller allows
AUTH_ROLE_PRODUCTS(ecommercen/eshop/controllers/Adv_vats_admin.php:22-28) but the admin menu hides the link from that role (application/config/admin_menu.php:519-525). Security through obscurity: a PRODUCTS user reaching the URL directly can still operate the CRUD. - No foreign key constraint —
shop_product.vat_idhas no DB-level FK. Out-of-band SQL, migrations, REST deletes, or the pharmacy auto-insert can orphan products with no database protection. shop_prices_viewINNER JOIN— orphaned products silently vanish from storefront listings. (database/initial/initial.sql:2451-2467)- Pharmacy ERP auto-insert is a hidden second writer —
ecommercen/helpers/farmakon_helper.php:100-122creates VAT rows outside the validator, with string-match-based dedup that is prone to typos. - Dead code paths may mislead future devs — Greek-islands branch in
AdvVatForOrder::setDeliverAreaType(), unused$isDigital, never-read$chargeAreaTypeproperty (set but ignored byvat()/invoiceVat()/receiptVat()), and unimplementedinvoiceVat(). (ecommercen/eshop/libraries/AdvVatForOrder.php) invoiceVat()is//TODO: not yet implemented— invoice-specific VAT logic never landed. (ecommercen/eshop/libraries/AdvVatForOrder.php)- Hard-coded marketplace shipping VAT —
application/config/app.php:529stores'shipping_vat' => 24, divorced fromshop_product_vats. - Pharmacy accounting hard-codes VAT strings —
ecommercen/helpers/farmakon_helper.php:283-318branches on literal'6.00' / '13.00' / '24.00'. Adding a new rate silently breaks pharmacy-accounting mapping. No REST RBAC— Fixed (GH #10).application/config/rest_policies.phpnow scopesVat::classwrites to[AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS](matching the legacy admin controller), while reads (index,show,item) remain public.- Cache not invalidated on admin write —
Adv_vats_adminnever callsclearCache('products'), so storefront prices remain stale until the pscache TTL expires.
Tests
| File | Covers |
|---|---|
tests/Integration/Domains/Order/Vat/RepositoryTest.php | Read repository queries |
tests/Integration/Domains/Order/Vat/ServiceTest.php | all(), item(), get(), plus create_persists_vat, update_modifies_value, delete_removes_vat, and round_trip. Seeds IDs 99001 (0%), 99002 (13%), 99003 (24%). |
tests/Unit/Domains/Order/Vat/ServiceTest.php | Unit-level read service coverage |
tests/Unit/Domains/Order/Vat/WriteRepositoryTest.php | hasReferences(): returns true when products reference the VAT, false when none (7049a4555). existsWithValue(): returns true on match, false when absent, excludes $excludeId row on update (GH #7). |
tests/Unit/Domains/Order/Vat/ValidatorTest.php | validateForDelete(): passes with no refs, throws ValidationException with 'id' key when refs exist (7049a4555). validateForCreate(): rejects null value, out-of-range values, >1 decimal place, duplicate values (GH #7). validateForUpdate(): allows null value, rejects range/precision/duplicate violations, excludes self from duplicate check (GH #7). |
tests/Unit/Domains/Order/Vat/WriteServiceTest.php | delete(): validator called before repo, repo skipped when validator rejects, returns true on success (7049a4555). update(): passes $id to validateForUpdate() (GH #7). |
tests/Legacy/Eshop/AdvVatForOrderTest.php | Only production-side caller of the dead Greek-islands branch |
Related Flows
- AD-02 Product Management Admin — products reference a VAT rate via
shop_product.vat_id - AD-03 Order Management Admin — order totals include
total_vatcomputed from captured line VAT - AD-13 Admin Settings —
enableOrderVatManipulationis a config flag, not a registry value, but lives alongside other platform toggles - CF-06 Order Preview — cart/checkout applies VAT via
AdvVatForOrder - CF-07 Order Confirmation —
shop_order_basket.product_vatcaptured at confirmation time - IN-01 Feed Generation — marketplace feeds use the hard-coded
shipping_vatconstant