All notable changes to PRAHO Platform will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Automated node DNS provisioning — deploying a node now creates its own Cloudflare A (and AAAA) records during the
configuring_dnsstage, before panel installation, and removes them on every teardown path (interactive destroy, the immediate failed-deploy compensator, and the scheduled cleanup sweep). Records are tagged to the owning deployment so an operator's manual records are never touched, replay duplicates converge to one, and a partially-created record is never leaked; a missing DNS token/zone fails before any paid server is created, and an unauthorized or wrong zone fails closed with the just-provisioned server compensated. Making the panel's Let's Encrypt certificate issuance fatal is tracked separately (#347, #436) - Staff API-token management — staff can list, create, and revoke only their own active API tokens from Settings; raw credentials are displayed once on a non-cacheable response, while token descriptions now round-trip through both issuance and identity API endpoints (#244, #247)
- Support ticket lifecycle policy — operators can enable an hourly, concurrency-safe policy that auto-closes only stale tickets waiting on a customer, records a dedicated resolution, and sends the customer a transactional notification; first internal notes no longer claim untriaged tickets, unknown first-reply actions fail before mutation, and all close paths clear stale customer-reply indicators (#239, #381)
- Audited billing operator controls — billing/admin staff can edit the live payment retry cadence and dunning-email behavior with validation, optimistic locking, required reasons, and rollback-safe audit evidence; retry offsets are anchored to a persisted definitive failure time rather than payment creation, with retry ownership confined to recurring convergence and invoice dunning; the same Settings surface exposes read-only invoice sequence state and a guarded legal-series rotation with no free-form counter edits or prefix reuse (#378, #379)
- Keyed audit integrity (v2) with no downgrade path — every audit event is stamped with an HMAC-SHA256 over the evidence itself (action, actor, object identity, old/new values, metadata) keyed via a dedicated derivation domain with key-id rotation; after the batched
restamp_audit_integritycutover a missing or downgraded marker verifies as compromised, GDPR anonymization no longer raises false criticals, and a verification that crashes persists an error-status check row and exits non-zero instead of defaulting to healthy (#217, #313) - Enforced audit immutability — AuditEvent mutation through any ORM path raises unless the call site enters the
audit_mutation_allowed()escape hatch with an auditable reason; every legitimate mutator (GDPR erasure/anonymization, retention, integrity stamping, reset cleanup) is enumerated and tested, and ContentType deletion can no longer cascade audit evidence away - Audit retention that actually runs — one policy-driven engine with seeded per-category policies (10-year mandatory financial retention, 5-year mandatory GDPR-accountability anonymization), DB-enforced mandatory-policy semantics, an effective financial cutoff, an allowlist-based anonymization invariant that re-stamps scrubbed rows, weekly scheduling, and durable file-integrity baselines that survive cache flushes with an explicit
--rebaselinedeploy step - Audit coverage and event-stream hygiene — payment collection runs audit their lifecycle and can no longer hang in "running" after a crash; a generic FSM-transition receiver audits every state change outside an explicitly tested skip-list; per-save double-writes are gone (order items, Virtualmin accounts, tax profiles per #241 — which also stops VIES re-validation on unrelated saves); GDPR export records survive user deletion; the audit management hub and saved-search subsystem are reachable from staff navigation (#241, #385)
-
Dead SIEM transport stack — ~2,300 lines of never-wired outbound SIEM code (transports, buffering, duplicate integration and file-integrity services, two orphan models) are deleted; the audit module stands alone with a structured JSON log stream and on-demand CEF/LEEF/JSON/Syslog/OCSF export as the integration surface (ADR-0043)
-
Settings catalog with a consumer contract — every runtime setting is declared once in
apps/settings/catalog.py(type, default, UI placement, input kind, sensitivity, criticality, validation) and a tokenize-based guardrail shared by CI lint and tests fails the build when a catalog key has no consumer or a consumed key is undeclared; 68 decoy settings that changed nothing (including the misleading editable VAT rate) were retired with a data migration, and 8 consumed-but-undefined keys (including the S3 backup credentials that silently resolved to None) are now declared (#238, #326) -
Three-surface settings UI — one
/settings/experience with Business, Integrations, and Platform zones: dirty-only atomic change-set saves with optimistic-concurrency baselines and audit reasons, write-only credentials with test-connection actions, catalog-driven search with deep links, per-setting audit history, and an Automation page showing every scheduled task with its last outcome; booleans can finally be switched off, JSON values round-trip, and secrets are never rendered or logged -
Enforced maintenance mode — a staff-exempt 503 middleware driven by the
system.maintenance_moderuntime setting with an environment override, replacing a decorative flag whose gate was never invoked -
Customer-controlled recurring payments — customer owners and billing members can authorize a saved card through a verified Stripe SetupIntent after the current terms version is accepted server-side and bound to the exact accepting principal and terms hash, enroll or remove individual services, and withdraw authorization without cancelling sibling services; PRAHO preserves the exact accepted agreement text with its version, hash, actor, timestamp, IP address, and user agent, and groups compatible renewals into one proforma and charge
-
Configurable renewal invoice lead time — operators can schedule fixed-renewal proformas 7–30 days before the paid-through boundary, with the existing 14-day behavior preserved by default and unprepared subscription schedules reconciled when the setting changes (#382)
-
Configurable domain renewal notices — operators can define a validated descending reminder schedule, while corrupt stored policy falls back safely and every configured threshold remains eligible exactly once (#383)
- Domain TLD and renewal-page correctness — configured multi-label suffixes now win by longest match across registration, transfer, availability, order, model-validation, and sample-data paths; registration and renewal periods enforce each TLD's own limits before registrar or order writes; and the renewal page renders valid markup with policy-allowed, renewal-priced, exact calendar-year options (#237, #285)
- Explicit invitation security policies — initial membership invitations, welcome-invite resends, and join-request owner notifications now have separate accurately scoped limits; deployed overrides migrate without being reset, and the initial-invitation gate reads its runtime setting instead of a hardcoded value (#384)
- Status visibility and accessible filters — the platform order dashboard now accounts for every current Order status without preserving removed refund states; the portal shows customer-scoped counts on every service-status tab; and shared HTMX tab filters support roving focus, Left/Right/Home/End navigation, and explicit tabpanel relationships (#99, #252, #279)
- PRAHO owns recurring billing — subscriptions, calendar-anchored periods, Romanian proformas/invoices, local usage rating, dunning, entitlement, and cancellation now use one PRAHO scheduler; Stripe Subscriptions, Prices, Meters, Stripe-side usage records, and the dormant staff-controlled customer auto-pay flag were removed, while Stripe remains the SetupIntent/PaymentIntent processor (ADR-0039, #209, #218, #219, #301)
- Romanian e-Factura scope — every Romanian PRAHO invoice, B2B and B2C, must enter the submission workflow regardless of total; obsolete segment enable flags and 100 RON/minimum-amount controls were removed, while the statutory five-working-day submission deadline remains enforced. B2C invoices and credit notes now use ANAF's consumer endpoint, supplied CNP values are frozen onto billing documents, and BT-47 carries either that CNP or the statutory 13-zero value when the buyer has no fiscal identifier
- Canonical e-Factura submission — removed the duplicate legacy submission engine and debug simulation path; generic billing tasks and compatibility callers now use the audited
EFacturaDocumentlifecycle, enqueue exactly once after the invoice transaction commits, and propagate submission failures instead of reporting false task success - Unsafe plan-change engine retired — historical
SubscriptionChangerecords remain readable, but the unwired executable path was removed because it changed billing without coordinating provisioning, never applied scheduled changes, and discarded downgrade credits; normal subscription renewal is unchanged
-
Order-driven domain renewals no longer collapse distinct intents — the renewal idempotency key was
{domain}:{years}, so a legitimate second one-year renewal of the same domain inside the cache TTL replayed the first cached result instead of contacting the registrar: the customer is billed for a renewal that never happened and the expiry never moves.renew_domainnow accepts an optionalidempotency_tokennaming one intent, and the order path passes theDomainOrderItempk — durable, exactly one per item, so a retried batch still replays rather than re-charging while two separate order items stay distinct. Callers with no durable identifier (the web renewal view) keep the previous key shape deliberately: minting a fresh token per call would remove double-submit protection entirely, which is the worse failure at a registrar that charges per renewal. That path remains tracked in #259. -
Domain-order renew items are processable and unhandled actions are logged —
create_domain_order_itemnever linked the existingDomainonto arenewitem, soprocess_domain_order_items'elif item.action == "renew" and item.domainguard was never true and every renewal was silently skipped; renew items now link the customer's ownedDomainwhen it exists, and the processing loop logs loudly for an unlinked renew, warns that order-driventransferitems are not auto-processed yet (rather than dropping them), and logs any unhandled action instead of dropping it silently (#430, the last latent finding from #237). Renewal ownership is re-checked at processing time, sinceDomainOrderItemrows can be created outsidecreate_domain_order_item. These paths have no live caller yet — this is a fix-before-wiring change; per-item idempotency andDomain.namecanonicalization are tracked in #442. -
Cart review page correctness — the customer cart-review page rendered several HTMX responses into the wrong element (a half-finished Alpine migration): changing an item's quantity replaced the Order Summary with a stray cart widget and never recalculated totals, removing an item turned its row into a full cart widget while leaving a stale product count and no empty-cart state, and a failed add-to-cart injected a duplicate
#cart-widget. Quantity changes now update the Order Summary in place, removals re-render the items list so the count and empty-cart state stay correct (including removing the last item), add-to-cart errors show a widget-free notice with correct English/Romanian labels, and dead mini-cart toggle code plus a redundant second totals request were removed. -
Async email attachments no longer bloat the task broker —
EmailService.send_email(async_send=True)forwarded full attachment bytes intosend_email_taskvia the django-q2 Postgres ORM broker, where they were pickled + base64-encoded (~1.33×) intodjango_q_ormqand re-persisted on every retry, so a burst of PDF-bearing proforma emails could hold many multi-hundred-KB rows in the queue table at once; attachments whose total size exceeds the newnotifications.max_async_attachment_kbsetting (default 5 MB,0to disable) are now sent synchronously in-process so their bytes never transit the broker, with delivery preserved (#358). -
make installworks in a git worktree —scripts/patch_precommit_hook.pyhard-codedHOOKS_DIR = pathlib.Path(".git/hooks"), but in a worktree.gitis a file pointing at the real gitdir, so the path didn't resolve and the hook-patch step failed the whole target with "run this script from the repo root" even thoughuv sync+ pre-commit install had succeeded; the hooks directory is now resolved viagit rev-parse --git-path hooks, which returns the shared hooks dir in both a normal checkout and a worktree (#386). -
make pre-commitno longer prunes the virtualenv — the fallback branch that bootstraps a missing pre-commit binary ranuv sync --group dev, butuv syncis declarative and removes every package outside the requested group set, so it stripped theplatform,prod, andportalgroups (65 packages, leaving exactly 124).django-cors-headerslives in theplatformgroup andcorsheadersis listed in platform'sINSTALLED_APPS, so a subsequentmake devdied insidedjango.setup()withModuleNotFoundError: No module named 'corsheaders'before running a single migration, and becausedev-allrunsmake -j2the result was a half-up stack with portal serving :8701 and platform dead on :8700; the branch now syncs--all-groups, which still provides the pre-commit binary it exists to install. Two setup docs that recommended equally narrow sync commands now point atmake install. -
Tailwind design system reconnected to the build — Tailwind v4's CLI has no
--configflag, soshared/tailwind.preset.jsand bothtailwind.config.jsfiles were never read: building with a nonexistent config path still exited 0, and the two service bundles were byte-identical despite declaring different content globs and differentprimaryhues. Three features silently resolved to nothing — everyprimary-*utility (20 distinct selectors across 50+ template references), the@tailwindcss/formsclassesform-input/form-select/form-checkbox(23 references), and theprose*typography classes (8 templates, including ticket comment and reply bodies where server-rendered HTML has no per-element classes to fall back on). Neither plugin was even installed, so the preset would have thrown onrequire()had anything loaded it. Theming moves to CSS-first entry points atassets/css/{platform,portal}.cssusing@themeand@plugin, and the four inert config files are deleted — deleting them produced byte-identical CSS, which is what proves they contributed nothing. Only referenced-but-unemitted tokens were ported: the old configs also overrode Tailwind defaults (borderRadius,fontSizeline-heights,boxShadow,screens) that never reached a browser, so the UI was built and reviewed against Tailwind's defaults and porting them now would restyle the app rather than fix it. Dark mode deliberately stays onprefers-color-scheme— the config asked fordarkMode: 'class'but no.darkclass is ever set on<html>, so switching strategies would stop all 578dark:utilities from applying to anyone (ADR-0035 amended). -
make devfails fast on a pruned virtualenv — when theplatformdependency group is missing, Django died insidedjango.setup()with an opaqueModuleNotFoundErrorbefore running a single command, anddev-all'smake -j2turned that into a half-up stack (portal serving :8701, platform dead on :8700) that reads as a hang rather than a crash. Acheck-venv-platformguard now gates bothdev-platformanddev-alland reports "runmake install" in one line before anything starts, at a cost of 36ms. Portal deliberately gets no equivalent guard: its group holds only gunicorn, whichrunservernever uses, plus duplicates of base[project].dependencies, so it starts fine on a pruned venv and gating it would be a false alarm. -
Coupon redemption records the post-discount order total —
CouponRedemption.order_total_centsdocuments the total after the discount, butmark_appliedomitted the field from itsupdate_fields, so the pre-discount total set at row creation was never overwritten and analytics/audit snapshots stored the wrong figure; the field is now persisted (#233). (The lock-scope race in the same issue is already fixed on master; the "re-apply a removed coupon" behavior is deferred to a separate design ticket because theunique_coupon_per_orderDB constraint makes it a schema decision, not a bug fix.) -
Virtualmin recovery/restore no longer reports fake success —
test_recovery_capabilityhardcodedrecovery_ready: Trueand a "✅ ready for disaster recovery" message whenever a dry-run rebuild succeeded — even when its own connection test had failed — so operators trusted a 0%-functional capability; readiness now requires a reachable server and the message is explicit that a dry run only checks data presence.restore_domaincollected per-component restore errors and then finalized as "Restore completed successfully" regardless, so a partial or total restore failure looked successful; component failures now roll back and surface as an error. First fail-closed pass on #326 — the remaining backup-path/S3-layout/rebuild findings are tracked as follow-ups. -
Virtualmin backup pipeline is honest about the remote archive —
_execute_full_backupwrites the archive on the remote Virtualmin host (the dest is passed to thebackup-domainAPI), but the returned dest was discarded sometadata['backup_path']was never set, and verify/upload did a localos.path.existson a reconstructed token-less path — every backup failed with a misleading "Backup file not found" and nothing reached S3. The real dest is now recorded and flaggedbackup_location: remote, and verify/upload fail with an explicit "remote retrieval not implemented (#326)" error instead of a false local failure; S3 metadata is written after the archive so an interrupted backup no longer leaves listable-but-unrestorable metadata. Third #326 pass (containment/honesty); the remote→local (or direct-to-S3) transfer and remote temp-archive cleanup are deferred to a dedicated transport ticket. -
Virtualmin server rebuild can actually rebuild —
rebuild_server_from_prahocalledcreate_virtualmin_accountfor each survivingVirtualminAccountrow, but that method rejects any domain that already has a row (already exists in PRAHO) — the row being rebuilt is the collision — so 100% of accounts failed and disaster recovery restored nothing exactly when it was needed. Rebuild now reuses the existing rows via a newreprovision_virtualmin_accountpath (fresh password, retargeted server, same server-side create workflow), andcurrent_domainsis reconciled from the authoritative active-account count instead of being clobbered to a stale value. Second pass on #326 (the CRITICAL rebuild finding). -
Node deployment guardrails — initial and retry deployments now receive a three-hour worker budget while the ORM broker waits four hours before redelivery, preventing duplicate execution of legitimate long-running installs; web, CLI, and orchestration paths reject missing, malformed, or oversized DNS zones before creating records, consuming retries, changing deployment state, or calling external systems (#362, #365)
-
Notification delivery and targeting — requests-compatible transport options now reach the DNS-pinned send path without weakening TLS, redirect, or timeout policy, so SNS certificate verification no longer fails on its timeout override; inactive and all-customer campaign audiences now select what their names promise, while the undefined trial-expiry audience fails closed instead of broadcasting (#215, #216)
-
VAT quote and evidence integrity — cart and preflight calculations now use the same customer tax profile and billing country as issued documents, incomplete VAT contexts fail closed, VIES checks retain attributable consultation references and fresh timestamps, expired validations are fully scheduled and drained, and PDF/e-Factura reverse-charge notices share the Article 196 legal basis (#404)
-
PostgreSQL order billing fixtures — persistence-reaching order transition tests now use ISO country codes that fit the billing document schema (#315)
-
Billing document completeness and e-Factura R051 validation — the portal now searches, filters, counts, and paginates invoices and proformas against the customer's complete server-side document set instead of two independently truncated 20-row slices; CIUS-RO validation now rejects every R051 amount context that omits or mismatches the document currency while preserving the BT-111 accounting-currency exception (#227, #371)
-
Billing/customer migration integrity — the address-role migration keeps a distinct current billing address authoritative, with a corrective repair for already-deployed flags; legacy refund backfill preserves the multiplicity of legitimate same-amount refunds, reconciles only against completed refund rows, isolates per-entity failures, and retains malformed or conflicting evidence for manual review instead of discarding it (#242)
-
Billing credit-score accounting — entering a partial or full refund state now applies the configured refund adjustment exactly once and leaving refunded states reverses its exact payment-bound score delta; deleting a succeeded payment reverses its actual clamped/bonus-inclusive adjustment, and successful payments without a due date count as on time consistently across aggregate and consecutive-payment scoring (#225)
-
Durable recurring-charge submission and recovery — recurring PaymentIntent authorization now commits as database state before Stripe I/O, so concurrent workers cannot abandon or duplicate an already-claimed attempt and customer withdrawals never wait on provider latency. A leased, bounded 10-minute reconciler replays durably claimed unbound requests with the original idempotency key and converges missed success/failure webhooks—including proforma conversion, entitlement, retry, and dunning—only after validating immutable Stripe facts; unbound pre-migration attempts are quarantined for manual Stripe review instead of being guessed safe to charge (#335, #409)
-
e-Factura submission and response evidence — ANAF uploads now acquire a committed, leased database claim before network I/O, never replay ambiguous POST outcomes, quarantine crashed/lost-response attempts for operator reconciliation, and freeze the claimed XML bytes; accepted
/descarcareresponses are validated in memory and stored byte-for-byte as ZIP archives with SHA-256 integrity metadata instead of being mislabeled as signed PDFs (#351, #352) -
Billing task reliability — scheduled billing batches now report partial failures truthfully; trial, grace, renewal, and paid-order fallback processing isolate and bound failures without losing audit evidence; stale renewal cycles fail closed; and Stripe refund reconciliation uses a distributed lease, discovery-first deduplication, and an operator-configurable hard work budget (#317, #344)
-
VAT evidence consistency — customer-specific VAT rates are now explicit optional overrides (with legacy generated defaults migrated safely), legitimate 0% order and invoice lines are preserved, recurring proformas use one billing-country snapshot for tax and document fields, cart totals retain fractional rates, Stripe no longer invents 21% metadata, and dormant duplicate VAT reporting/configuration paths were removed (#389, #390)
-
Discounted-order VAT convergence — document allowances now reduce the taxable base before VAT across order calculation, preflight, proforma issuance, and direct-invoice recovery; mixed-rate discounts fail closed until per-rate allocation is supported, and issued proformas remain the authoritative amount throughout Stripe confirmation and fallback conversion (#203)
-
Portal service usage identity — usage requests now sign the authenticated user ID separately from the active customer ID, so platform membership validation succeeds instead of silently rendering an all-zero chart; shared identity extraction also fails closed on missing or malformed session IDs across service views (#230)
-
Refund lifecycle convergence — refund initiation now follows the canonical Payment → Invoice → Order lock order, reserves pending as well as settled amounts, and projects balances only after gateway settlement; Stripe refund webhooks and a scheduled discovery sweep converge idempotently through one service, including dashboard-created and legacy charge refund events, while a conditional unique constraint prevents duplicate non-empty gateway refund IDs (#196, #323, #324)
-
Provisioning retry safety — Virtualmin and generic server gateways now classify retry outcomes explicitly; read-only calls may retry ambiguous transport failures, while mutations replay only when rejection is proven. Retriability metadata now survives gateway, authentication, service, task, job, and infrastructure boundaries, preventing lost responses or exception text from double-executing provisioning actions (#253, #254, #262)
-
Promotion discount concurrency — coupon application, coupon removal, and gift-card redemption now lock and refresh the order before changing its discount total, preventing concurrent redemptions from being debited or recorded while their value is lost from the order; gift cards re-validate on the locked row, enforce their start date, and refuse cards deleted mid-redemption (#310)
-
Virtualmin provisioning pipeline — the pipeline can now actually provision: the queued task no longer crashes on every dequeue (invalid django-q2
retry=kwarg), account creation no longer dies at pre-flight (wrong-class health check plus conflict/template checks that read response keys the parser never produced), and the failed-job sweep uses a leased-claim protocol with a job-aware retry that converges timed-out-but-remotely-successful creates on the existing rows instead of stranding jobs inpendingforever. Drift records persist with real model fields instead of raisingTypeErrorexactly when drift exists. Server health no longer starves placement (10-minute upserted schedule, success-only freshness stamp, streak-based reversible auto-fail instead of single-strike permanent eviction). Service suspension/termination/reactivation now propagates to Virtualmin through an idempotent on-commit reconciliation task — suspended customers no longer keep live hosting — and the ADR-0019 emergency kill switchVIRTUALMIN_AUTO_PROVISIONING_ENABLEDexists at last (#325) -
Recurring-charge revocation ordering — final authorization and a durable submission claim now share a PostgreSQL boundary with mandate withdrawal, cancellation, renewal opt-out, and the global kill switch; revocation-first attempts abort before Stripe, while claim-first attempts remain authorized without forcing the opt-out to wait for provider I/O. Neither the kill-switch nor customer row remains locked across Stripe, so one hung call cannot stall customer controls or unrelated collection (#316, #335)
-
Refund gateway integrity — gateway failures now fail closed before local refund state changes; order refunds resolve the authoritative invoice/proforma Payment, persist gateway references and the payment currency, send exact cent amounts, and keep cumulative Payment/Invoice refund states consistent (#212)
-
Order-to-renewal continuity — paid recurring orders now create exactly one PRAHO subscription linked to the service, preserve the order item's financial terms, synchronize the initial paid-through entitlement, reject mismatched or unpaid enrollment retries, and correctly disable renewal on immediate cancellation or full initial-invoice refund while preserving suspension evidence
-
Subscription renewal payments — recurring billing now charges authoritative balances through a fail-closed global switch and active customer mandate; revalidates the payment method, mandate, and renewable service lifecycle at the final pre-Stripe boundary; serializes automatic attempts against manual settlement and cancellation; creates a new PaymentIntent for every policy-controlled retry; strictly reconciles webhook facts; and advances, suspends, expires, or cancels each service exactly once (#209, #301)
-
Local usage billing — usage events resolve to their real timestamped billing cycle and stay pending on missing or ambiguous ownership; periods must be positive and non-overlapping, rating requires one effective non-negative, contiguous price schedule in the subscription currency at period start, snapshots the exact schedule with banker's-rounded cents, and rolls back the whole cycle on any failure; invoice/finalization workers lock and recheck their cycle, prepaid fixed invoices and post-paid usage invoices are separate, authorized usage collection waits until seven days after period end and safely resumes interrupted attempts, zero-usage cycles do not create invoices, and cumulative hosting snapshots cannot feed additive meters (#218)
-
Infrastructure drift remediation convergence — the drift subsystem now converges instead of looping: post-remediation verification polls the provider for the actual outcome with a boot grace and bounded retries (a booting server no longer false-negatives into a destructive snapshot rollback); drift without an automated fix (IP/network/server-deleted) is routed to explicit manual-intervention requests instead of no-op "remediations"; crashed or orphaned executions are auto-recovered by a periodic reaper and can never permanently block a deployment (conditional-update claims + partial-unique DB constraints); scans dedup into one open report per drifting field with occurrence tracking, externally-fixed drift is healed and its requests superseded, and accepting drift now durably writes the observed value back (IP addresses, node size, powered-off state) — fields with nothing durable to write can no longer be "accepted" into a 15-minute reappearance loop; the destructive snapshot restore re-validates at the moment it acts — an unobservable provider (API outage) routes to manual intervention instead of a rebuild, a failed apply cross-checks actual provider state first, drift healed mid-execution skips the restore and keeps its resolution, and the scanner can no longer overwrite the values of a concurrently-approved request (#224)
-
Billing money-state atomicity — paid multi-service and order-confirmation transitions now roll back atomically; concurrent Stripe customer creation and webhook metadata writes preserve unrelated state and established customer IDs; Stripe customer and payment-intent creation use deterministic idempotency keys; and payment reuse is scoped to the authoritative order, gateway, amount, and currency to prevent partial activation, lost updates, and duplicate or stale payment intents (#240)
-
Billing document adjustment integrity — invoice and proforma recalculation now runs only on eligible undiscounted drafts, while e-Factura XML and customer PDFs reject metadata allowances/charges and dormant line discounts that are not represented in PRAHO's ledger totals; stored document discounts and legacy reconciliation remain unchanged (#195)
-
e-Factura prepaid balance — partially-paid invoices now emit PrepaidAmount (BT-113) and a PayableAmount (BT-115) reduced by net collected payments (PayableAmount = TaxInclusive - Prepaid, BR-CO-16), derived from the refund-aware remaining balance. Completes the remaining scope of #178. (#177)
-
e-Factura setup fees + document discounts — represented document-level discounts (BT-92/BT-107 via a stored, frozen
discount_centsand a BG-20AllowanceCharge) and setup fees (as their own invoice line), so discounted/setup invoices reconcile (BT-106 = Σ line BT-131, TaxExclusive = TaxableAmount = net, BR-CO-10/13/14/17). The discount is derived from the totals invariant so legacy invoices reconcile without a backfill, and all three e-Factura XML paths (ANAF submission, staff download, legacy submission service) now route through the single canonical builder. Refund-aware invoice remaining balance (#189). (#188, #189) -
e-Factura EN16931/CIUS-RO XML compliance — completed the UBL 2.1 builder for ANAF Schematron conformance: EU cross-border B2B encoded as reverse charge (AE, VATEX-EU-AE, rate 0, all lines coherent per BR-AE-1) with deterministic tax-category derivation; corrected VATEX exemption codes (S/Z emit neither BT-120 nor BT-121; BT-121 only with a valid VATEX code); per-line and credit-note VAT rate clamped to 0 for non-standard categories; credit-note unit code from the line. Ledger-backed document discounts reconcile in UBL element order with consistent TaxExclusive/TaxInclusive/Payable and a netted breakdown TaxableAmount (BR-CO-13/15/16/17), while unsupported metadata adjustments fail closed (#158, #160, #195)
- Final
is_staff → is_staff_usersweep sites resolved (#271 complete) — the two sensitive boundaries are now settled: the HMAC/session-auth bypass for the ticket form's/api/customers/GET admits role-only staff (staff_roleset,is_staff=False) via the canonicalis_staff_user, fixing a broken customer-services dropdown; and credential-vault decryption is tightened to superusers and admin-role staff only, so support/billing/manager roles (and a bareis_staffflag) can no longer read Virtualmin/SSH/cloud secrets. Nativeis_staffnow carries no authorization role in the platform (Django admin is disabled), matching the documented intent that business access derive fromstaff_role(#271). - Consistent staff classification for audit search suggestions —
AuditSearchService.get_search_suggestionsgated user-search auto-complete on nativeis_staff, so role-only staff (staff_roleset,is_staff=False) — who already pass the endpoint'sstaff_required_strictguard — were silently denied suggestions; the gate now uses the canonicalis_staff_user, resolving one of the remainingis_staff → is_staff_usersweep sites (#271). (The two sensitive-boundary sites are resolved in the entry above.) - API abuse-protection proof — order, catalog, and session-validation endpoint limits now execute on DRF function views instead of silently no-oping, protected HMAC views retain their global per-portal sustained and burst limits, and PostgreSQL CI proves concurrent API-token issuance cannot exceed the per-user cap while usage tracking writes at most once per interval (#229, #248)
- API token boundary hardening — direct token authentication now rejects malformed raw-key formats before hashing and returns one generic failure for unknown, expired, or disabled-user tokens; the per-user active-token cap joins the TTL policy as startup-validated deployment configuration (#249, #250)
- Virtualmin and infrastructure control plane — Virtualmin credentials now require HTTPS and either CA verification or a SHA-256 certificate pin enforced during the TLS handshake; auto-registered and existing self-signed nodes obtain their pins through verified SSH rather than network trust-on-first-use; all SSH and Ansible paths reject unknown host keys from a shared operator-provisioned trust file; root maintenance accepts only fixed semantic actions and elevated deploy permission; authentication fallbacks escalate only on typed authentication failures and preserve retry-safety metadata; rate limiting uses atomic fixed-window slot claims; and SSH key generation and revocation are audited and fail closed during node destruction (#327)
- Order → Proforma → Invoice lifecycle — full document lifecycle with EN16931 e-invoicing compliance (#138)
- Platform orders dashboard — order dashboard with stats, number formatting, slug display, and responsive tables (#99, #132)
- Customer pages redesign — complete customer pages redesign with Portal parity (#139)
is_staff_userconsolidation — introducedis_staff_user(bool(staff_role) or is_staff or is_superuser) as the canonical staff-classification property and migrated the codebase off the inconsistentuser.is_staff/user.is_staff or user.staff_rolepatterns: backend and decorators (#175), ticket templates (#151, #159), and the remaining non-ticket templates — base nav, dashboard, billing list/proforma, customers header, user profile, mobile header (#176, #180). Support agents (staff_role="support",is_staff=False) now see staff-only UI (reopen button, internal notes, list filters, customer column, refund actions) while customers remain excluded. Added render-level regression tests covering the discriminatingis_staff=Falsesupport-agent case
- Cart totals — batch-fetch products and prices in
calculate_cart_totals, removing per-item queries (#100, #161) - Portal auth — reduced auth log noise and enabled HTTP connection reuse (#83, #164)
- Invoice refund & mobile nav gates —
billing/invoice_detail.htmlgated the staff "Refund Invoice" button/modal on an undefinedis_staffcontext variable (rendered for no one, fail-closed);components/mobile_nav_item.htmlgated staff-only items on the Djangois_staffflag (locking out support agents); both now gate onuser.is_staff_user(#159, #180 review) - Billing & refund integrity — terminal statuses, row locking, refund source-of-truth, price seal, and order-item handling (#134); follow-up hardening for FSM gaps, TOCTOU, IDOR, and refund backfill;
ObjectDoesNotExistinstead of string matching in refund lookups (#120, #131);InvoiceNumberingService.get_or_create_sequenceAttributeError; EN16931 field display, proforma UX, and standalone payment flow - Orders — status-change AJAX feedback and destructive-action confirmation (#98, #163); 302→login redirect handling and modal hardening;
transaction.set_rollback(True)in the create_order error path; PR #132 review findings (aggregate query, billing address, Tailwind, fixtures) - Notifications — atomicity, audit-trail, and TOCTOU fixes for consent operations (#94, #162); marketing-consent audit dedup (#183); RSA public-key type narrowing in the SES webhook verifier
- Customers & API — invite email sent on Portal user creation (#145, #149); PR #149 review findings and tracking issue #173 (#184); 17 review findings from PR #164; 3 security and data-integrity gaps in the customer API (#143, #144, #146)
- Staff role classification —
is_staff_userproperty, validation, and a data migration cleaning legacystaff_role="customer"rows (#150, #154) - UI — nav icons, missing mobile menu items, and section dividers; 19 E2E failures (mobile nav button, selectors, ORM fixtures)
- CI & tests — use
config.settings.cifor nightly integration and cache tests; deduplicate imports intest_security.py; webhook regression and test-quality fixes (#137, #172)
staff_roleenforced at the database layer — added aCheckConstraint(user_staff_role_valid) so a truthy-but-invalidstaff_role(e.g."customer") can no longer be persisted via raw.update()/bulk_create/ fixtures / migrations and silently read as staff byis_staff_user; theUserManager.create_userguard remains as the application-layer check (#174)- Comprehensive security audit — resolved 22 CRITICAL and HIGH vulnerabilities from a full audit
- Pre-release hardening — all medium and low priority findings (#130, #137) plus 5 post-merge hardening fixes (#147)
- Auth bypass gating — gated the
TESTINGauth bypass onDEBUG=Trueto prevent production exposure (#129)
- django-fsm-2 migration (ADR-0034) — migrated 10 status-driven models to
FSMField(protected=True)with@transitiondecorators: Order, OrderItem, Invoice, ProformaInvoice, Payment, Refund, Subscription, Service, Domain, Ticket;ConcurrentTransitionMixinon Order and Service for race-sensitive paths TaxService— centralized Romanian VAT calculation consolidating 5 scattered tax snippets into one stateless service with VAT-compliant banker's roundingapps.common.financial_arithmetic— shared cents-based financial helpers (cents_to_decimal,decimal_to_cents,allocate_proportional) replacing ad-hoc Decimal arithmetic across billing- DB CHECK constraints — 22 new constraints across orders/billing/provisioning/domains/tickets enforcing valid status values and non-negative financial amounts at the database level
- FSM guardrail lint (
make lint-fsm) — AST-hybrid script detecting 5 bypass patterns (direct.status =,QuerySet.update(status=),bulk_update,__dict__bypass, side effects in@transitionmethods); integrated intomake lintpipeline and pre-commit - FSM transition smoke tests — 106 tests across all 10 models verifying valid transitions, invalid transition rejection (
TransitionNotAllowed), and protected field blocks (AttributeErroron direct assignment) force_status()test helper —tests/helpers/fsm_helpers.pybypasses FSM protection via__dict__for test setup; only allowed path for direct status manipulation in tests_TICKET_VALID_TRANSITIONSgraph — static FSM transition map derived from@transitiondecorators, used byget_allowed_transitions()to show only valid UI options per source status- EU VIES VAT validation — cross-border reverse-charge support with real-time VIES API validation for B2B EU transactions (#88)
- Ghost emails/provisioning on rollback (C1) — billing payment signal side-effects (
_send_payment_success_email,_trigger_virtualmin_provisioning_on_payment, etc.) now wrapped intransaction.on_commit()so they only fire after the DB transaction commits confirm_paymentswallowed FSM conflicts (C2) —TransitionNotAllowedin payment confirmation now returnssuccess=False, status="fsm_conflict"instead of silently returning the gateway'ssuccess=True- Refund entity updates were dead code (C3) —
_process_entity_updates()now actually calls_update_order_refund_status()and_update_invoice_refund_status()instead of setting*_updated=Truewithout doing anything - Order cancellation FSM bypass (C4) — replaced
QuerySet.update(provisioning_status="cancelled")with per-item FSMcancel_provisioning()transitions, now handling bothpendingandin_progressitems - TRANSITION_MAP dispatch by target only —
_ORDER_TRANSITION_MAPnow keyed by(source, target)tuple; fixes ambiguous dispatch wherefailed→pending(retry) anddraft→pending(submit) shared the same target key - Silent
contextlib.suppress(TransitionNotAllowed)inrefund_service.py— replaced with explicit try/except that logs warning and returnsErr()instead of silently leaving invoice in wrong status - Phantom audit logs in
payment_service.py—log_security_event("payment_status_changed")now only emits after successful FSM transition, not on failure or unmapped status - Issue-after-failure in
usage_invoice_service.py—TransitionNotAllowedoninvoice.issue()now returnsErr()instead of proceeding to setissued_at/locked_at refresh_from_dbfield loss on all 10 FSM models — addedfieldsparameter guard so FSM fields are only popped from__dict__when they'll actually be refreshed; prevents losing in-memory status when refreshing unrelated fields- Provisioning webhook FSM bypass —
provisioning/webhooks.pychanged fromservice.status = "active"toservice.complete_provisioning()FSM transition method - Ticket
get_allowed_transitions()— now returns only FSM-valid targets per source status instead of all map keys minus current (was showing invalid options like "in_progress" from "closed") - Auth
state_version→membership_hash— replaced stale state versioning with HMAC-based membership hash for more reliable session invalidation (#84) - Portal signed-cookie → DB sessions — switched from signed-cookie sessions to database-backed sessions, restoring SecurityMiddleware compatibility (#118)
- Refund aggregation fail-hard — refund service now raises typed DB exceptions on aggregation errors instead of silently returning zero (#119, #120)
- CORS config + refund error logging — fixed CORS middleware ordering and added structured error logging in refund service (#67, #63)
- Billing audit trail completeness — audit trail logging, aggregation safety, Sentry visibility with entity context in refund service
- CSP nonce deferral — Content Security Policy nonces now correctly deferred for Stripe scripts; improved error handling in payment confirmation
Resultclass deduplication — consolidated 3 identicalResultimplementations intoapps.common.types.ResultwithOk/Errconstructors- Unified design system (ADR-0035) — extracted shared UI components into
shared/ui/, migrated to{% icon %}tags, removed hardcoded SVGs and inline scripts across both services db_tablestandardization — all Django models now use app-prefixed bare plural convention (billing_invoices,orders_items, etc.) for consistency- psycopg v3 migration — replaced
psycopg2-binarywith nativepsycopg(v3) for async support and better connection pooling - Tiered CI testing (ADR-0036) — SQLite for PR checks, PostgreSQL for master/nightly; affected-module detection for focused test runs
- Pre-commit pipeline — added
fsm-guardrail-checkhook aftercode-health-check - README test badge — updated from 5,000+ to 7,000+
-
Row-bound encrypted payment data — bank details now use v2 AES-256-GCM with exact immutable per-row AAD on first insert and every ORM projection; cross-row ciphertext transplants, downgrade/plaintext values, malformed base64, empty AAD, and decryption failures fail loudly without replacing forensic ciphertext with NULL. Existing rows are re-encrypted by a reversible fail-closed migration, Stripe-only paths defer unrelated bank data, and the key-rotation cipher cache is bounded (#205, #267, #268; ADR-0040)
-
Domain webhook HMAC-SHA256 verification — implemented signature verification for domain registrar webhooks with timing-safe comparison
-
Order idempotency hardening — DB-backed unique constraint per customer with atomic
cache.addand stuck-lock recovery;IntegrityErrorcatch narrowed to only handle idempotency race conditions, not financial check constraints -
Payment intent validation — regex validation (
pi_[a-zA-Z0-9]{10,64}) at both platform and portal boundaries withisinstanceguard against non-string input -
Timing-safe HMAC — switched to
hmac.compare_digest()for signature comparison -
Order number generation race — replaced
count()-based TOCTOU race withMAX(order_number)+select_for_update(PostgreSQL lock with SQLite fallback) + retry onIntegrityError -
Cart DoS hardening — per-session rate limiting, quantity caps, total amount ceiling, duplicate-item rejection, payload size checks, suspicious pattern detection; cache fails closed
-
XSS hardening —
escapejsin 5 inline JS template contexts; sanitized error responses to stop leaking state machine internals -
Payment intent binding — reject PI binding for non-stripe orders; only create
PaymentIntentfor explicitpayment_method="stripe" -
UUID validation on
confirm_paymentorder_id anduser_idnull guard beforeint()cast
- EU compliance on checkout — terms acceptance validation (EU consumer law), privacy policy link, withdrawal notice (Directive 2011/83/EU), GDPR Art. 13 data retention notice
- Dynamic VAT rate — platform API returns
vat_rate_percentand per-item calculations; templates display actual rate instead of hardcoded 21% - Currency FK on Service model — required
ForeignKeywith 3-step migration (add nullable → backfill to RON → make required); all 4 service creation paths updated currency_codein service serializers — added toServiceListSerializerandServiceDetailSerializerto fixVariableDoesNotExistin templates- Stable cart API identifiers —
product_slug+billing_periodin cart calculation response for deterministic per-item mapping - "Confirmed" order status in admin — badge rendering, filter dropdown, status transitions, stats card
step_progressinclusion tag — unified component replacing 3 duplicates (step_navigation,progress_indicator,order_breadcrumbs); 3 variants (default/compact/vertical), 3 color schemes, WCAG accessible, optional back button- Per-item
line_total_centsdisplay on checkout page - ORDER_LIFECYCLE.md — domain documentation for order state machine
- ~8,500 lines of new tests — 6 platform test files, 8 portal test files, 3 E2E test suites covering all hardening changes
- Stripe skip for bank transfer — payment intent creation no longer fires when
payment_methodisbank_transfer - Decimal precision — cents conversion uses
Decimalinstead offloatto avoid IEEE 754 precision loss on amounts like 29.99 - HMAC middleware scoping — replaced broad
/billing/prefix with explicit tuple of 6 portal-facing API endpoints; staff billing UI pages no longer intercepted - Stripe failure redirect — redirects to confirmation (not checkout) since order already exists and cart is cleared
- Product type badges — store
product_typein cart item dict; guard empty values with{% if %}in 3 templates - ISO timestamp parsing — confirmation view parses ISO string to datetime
- Monetary display —
romanian_currencyfilter replacesfloatformat:2across services, plans, and dashboard templates - Pagination — hidden when only 1 page exists
- Checkout
is not None— uses identity check instead of truthiness so $0 free-trial items display correctly
- Breadcrumb UX — completed steps are now clickable
<a>links; arrow separators moved inside<li>elements - Cart rate limiting —
CartRateLimiterclass removed; rate limiting handled byAPIRateLimitMiddleware - Portal checkout refactored — extracted shared helpers for payment routing and flow guards
- 3 duplicate UI components —
progress_indicator.html,step_navigation.html,order_breadcrumbs.html(~580 LOC) replaced bystep_progress.html CartRateLimiterclass and related tests — superseded byAPIRateLimitMiddleware
str(e)information leakage (CWE-209) — replaced all 5return {"error": str(e)}patterns incustomers/tasks.pywith generic"Task failed, see server logs"message; exception details now only in server logs vialogger.exception()- JSONField race condition (CWE-362) — added
transaction.atomic()+select_for_update()narrow locks around allcustomer.metaread-modify-write operations intasks.py,credit_service.py,services.py, andbilling/invoice_service.py; prevents lost updates from concurrent Django-Q2 tasks - Stale-owner cache lock — replaced
cache.add(key, True)+ unconditionalcache.delete()incleanup_inactive_customerswith tokenized lock pattern (uuid4token, compare-and-delete infinally); prevents TTL-expired first worker from deleting second worker's lock Customer.metatype validation — two-layer enforcement: app-levelsave()coercesNone→{}and raisesValueErrorfor non-dict;clean()raisesValidationErrorfor form/admin; DB-levelCHECK (jsonb_typeof(meta) = 'object')constraint (migration 0008)- Fail-closed on corrupt cooldown dates —
cleanup_inactive_customersnow skips send (instead of fail-open) whenlast_reactivation_emaildate is unparseable, preventing unlimited reactivation email spam - New scanner rule:
exception-str-return(AST) —error_handling_scan.pydetectsreturn {"error": str(e)}inside except handlers; found 32 pre-existing instances in billing/ - New scanner rule: Unguarded JSONField write (regex, scoped) —
security_scanner.pyflags.meta[key] =mutations in**/tasks.pyas race condition risk - New scanner rule: Information Leakage (regex) —
security_scanner.pydetects"error": str(var)patterns exposing exception internals in return dicts
- MyPy type errors fully resolved — removed 27 stale
# type: ignorecomments acrossorders/services.py,billing/usage_invoice_service.py,common/validators.py,audit/siem.py, andusers/services.pythat became unnecessary afterSoftDeleteManagerwas made generic; fixed 10 genuine errors inrate_limiting.py(getattr annotations, unreachable code, no-any-return),infrastructure/views.py(4x request.user arg-type),billing/efactura/b2c.py(__all__for re-exports),common/key_derivation.py(SECRET_KEYNone guard),common/management/commands/post_encryption_upgrade.py(HostingAccount→VirtualminAccountbroken import), andapi/users/views.py(bool wrapper on DRF Any return); removing stale ignores inusage_invoice_service.pyexposed and fixed 5 hidden attribute errors from wrong field names - Magic number constants extracted —
refund_service.py_FALLBACK_ORDER_TOTAL_CENTSand_FALLBACK_INVOICE_TOTAL_CENTS;infrastructure/apps.py_CLEANUP_FAILED_INTERVAL_MINUTESand_RECOVER_STUCK_INTERVAL_MINUTES - Exception logging improved —
drift_remediation.pybareexcept Exception:blocks now capture and logefor observability;_get_cloud_gatewayaddslogger.erroron failure rate_limiting.pynosemgrep comment moved to inline position on theimportlib.import_modulecall line (cosmetic, no behaviour change)notifications/services.pysave()scoped —customer.save()now usesupdate_fields=["marketing_consent", "newsletter_consent"]to avoid full-row overwrites on unsubscribe- Code health scan integrated into
make lint(Phase 5) and exposed as standalonemake lint-health; pre-commit hookcode-health-checkadded to.pre-commit-config.yaml make test-securitycache check — removed spuriousos.environ.setdefaultcall;DJANGO_SETTINGS_MODULEnow set via env prefix- Engagement score weights from
SettingsServicenow clamped to[1, 100]to prevent division-by-zero or negative scores from misconfigured settings - Ticket open statuses in
cleanup_inactive_customersextracted to_OPEN_TICKET_STATUSESmodule constant (was inline list) security_scanner.py_matches_path_globnow supports comma-separated globs for multi-file-type scopingSoftDeleteManagernow generic — parameterised overTypeVar("_M", bound=models.Model)instead of hardcodedManager["Customer"]; subclass models (CustomerAddress,CustomerPaymentMethod,CustomerNote) now get correctly typedQuerySet[T]returns, eliminating ~20# type: ignore[misc]and# type: ignore[return-value]comments acrosscontact_service.py,contact_views.py, andprofile_service.pyCustomerPaymentMethod.save()simplified — removed prematureclean_fields()/clean()calls before the atomic block; Django's defaultsave()already handles validation- Forms use
TextChoices.choices—CustomerCreationFormandCustomerEditFormnow referenceCustomer.CustomerType.choicesinstead of legacyCUSTOMER_TYPE_CHOICEStuple - Tasks use raw string values —
start_customer_onboardingandcleanup_inactive_customerscompare against stored string values ("company","active") instead of enum members, matching what the DB actually stores - Removed unnecessary
cast()calls inCustomer.get_primary_address()andget_billing_address()— generic manager makes return types correct without casting
- Customer TODO stubs flushed — all 6 placeholder implementations replaced with real business logic:
process_customer_feedback: keyword extraction + category tagging with sentiment analysisstart_customer_onboarding/ onboarding steps: multi-step orchestration (welcome email → billing setup → verification)cleanup_inactive_customers: GDPR-aware inactive customer processing with reactivation emailssend_customer_welcome_email: delegates toEmailService.send_template_emailwith bilingual supportcustomer_services_api: wired to provisioningServicemodel with filters, pagination, and stats
- Shared CUI validator (
apps/common/cui_validator.py) — extracted from inline regex to reusableCUIValidatorclass (pattern matchesCNPValidator); accepts bothRO12345678and12345678formats; used by customers and billing apps @public_api_endpointmarker decorator (apps/api/secure_auth.py) — marks endpoints as intentionally unauthenticated; CI test enforces every API view has either this marker or an auth decorator@require_portal_authenticationdecorator (apps/api/secure_auth.py) — lightweight HMAC backup that verifies_portal_authenticatedflag; defense-in-depth for endpoints previously relying only on middleware- Structural CI test (
tests/api/test_api_auth_coverage.py) — scans all/api/URL patterns via Django URL resolver and fails if any view lacks an explicit auth decorator or@public_api_endpointmarker; walks DRF@api_viewclosure chains and CBVpermission_classes - Custom
@rate_limitdecorator (apps/common/rate_limiting.py) — drop-in replacement fordjango-ratelimitusing Django cache framework directly; supportsip,user,post:<field>,header:<name>, and dotted-path callable keys; 9 unit tests configure_rate_limiting()helper (config/settings/_rate_limiting.py) — single source of truth forRATE_LIMITING_ENABLEDsetting with env var override- 14 test cases for secret key resolution and production validation (
test_secret_key_resolution.py)
- Mass assignment vulnerability (OWASP A04:2021) — added
UPDATABLE_FIELDSfrozensets toCustomerService,ProfileService;update_customer()and profile update methods now filter against allowlists and log rejected fields User.objects.create()bypassing password hashing — changed tocreate_user()incustomer_create_userview- Missing role validation —
customer_add_userandcustomer_create_usernow validate role againstCustomerMembership.CUSTOMER_ROLE_CHOICES - Missing ownership validation —
set_default_payment_methodnow verifies payment method belongs to the customer - Address versioning race condition — wrapped in
transaction.atomic()+select_for_update()in both view and service layer - Form save atomicity —
CustomerCreationForm.save()now wraps 4 object creations intransaction.atomic() - Broken
unique_togetheron addresses — replaced with partialUniqueConstraint(condition=Q(is_current=True))for correct current-address uniqueness - CUI validation always returning False —
validate_cui()onCustomerTaxProfilewas passing RO-prefixed values to a validator that rejected RO prefix; now uses sharedCUIValidatorthat handles both formats - Credit scoring dead code —
get_base_credit_score()andget_credit_adjustments()were defined but never called;calculate_credit_score()now uses them instead of module constants _get_consecutive_on_time_paymentsnot checking due dates — was counting consecutive successful payments regardless of timing; now checksinvoice.due_at- Phantom order statuses — removed references to non-existent
"delivered"and"fulfilled"statuses from analytics queries - USD currency option — removed from billing profile form (model only allows RON/EUR)
- Soft-delete signal using filtered manager —
store_original_customer_valuespre_save signal changed fromCustomer.objects.get()toCustomer.all_objects.get()to handle soft-deleted record restore - Duplicate default payment method signal — removed redundant
_ensure_single_default_payment_methodcall (already handled in modelsave()) metadict access on None — addedcustomer.meta = customer.meta or {}guard before dict operations- Manager declaration order —
SoftDeleteManageris now first (default manager) for correct admin/reverse FK behavior - Missing
CustomerAnalyticsService.record_invoice_event— method called frombilling/signals.pybut never existed;AttributeErrorwas silently swallowed by broadexcept Exception. Added implementation usingAuditService.log_simple_eventpattern. SECURE_SSL_REDIRECThardcoded in prod/staging settings (#30) — Replaced hardcodedSECURE_SSL_REDIRECT = Truein all 4 prod/staging settings files with env-configurableDJANGO_SECURE_SSL_REDIRECT(default:true, secure-by-default). Strict boolean parsing with.strip().lower()andImproperlyConfiguredon invalid values. Logs warning when disabled. Addedsecurity.W060system check for disabled redirect in production. Updated 4 Docker Compose files, 3 Ansible templates, 2.env.examplefiles, and 3 docs to match.- SECRET_KEY env var mismatch (#19) — Standardized on
DJANGO_SECRET_KEYas canonical env var name across portal/platform settings, Docker Compose, CI workflows, Ansible templates, and env examples - DATABASE_URL vs DB_ mismatch* (#23) — Replaced unused
DATABASE_URLwith individualDB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORDvars in all compose files to match what Django actually reads - Healthcheck URLs in deploy.sh (#27) — Platform:
/health/→/api/users/health/; Portal:/health/→/status/ - No resource limits in Docker Compose (#38) — Added
deploy.resources(CPU/memory limits and reservations) to all production compose files - uv base image tag not pinned (#43) — Pinned from
:latestto:0.8.13in both Dockerfiles - Insecure
get_client_ipin price sealing (#57) — Deleted hand-rolledget_client_ip()and_is_valid_ip()fromprice_sealing.pythat trusted proxy headers from any source and used leftmost-IP (split(",")[0]); replaced withget_safe_client_ip()(trusted-proxy-aware, rightmost-trusted-hop) in views and serializers; added 6 regression tests and 3 new security scanner patterns for hand-rolled IP extraction - CNP validation tautological check (#64) — Extracted
CNPValidatorfrombilling/efactura/b2c.pytocommon/cnp_validator.pyfor reuse across platform and portal; fixed portal tautological digit check ("1234567890"→"123456789", digit 0 is not a valid century marker); upgraded platform form to full semantic validation (checksum, birth date, county codes); 21 tests added - No DB-level constraints on financial fields (#71) — Added non-negative
CheckConstraints onOrder(subtotal_cents,tax_cents,discount_cents,total_cents) andOrderItem(unit_price_cents,setup_cents,tax_cents,line_total_cents); app-layer validators can be bypassed by direct ORM calls — DB constraints cannot - EmailLog encryption fallback (#72) — Replaced raise-or-swallow encryption with graceful degradation per ADR-0017: added
body_encryptedfield with backfill migration;save()never raises on encryption failure (snapshots originals, restores on error, marksbody_encrypted=False, emits CRITICAL alert); removedALLOW_UNENCRYPTED_EMAIL_LOG_FALLBACKsetting; added deprecation system check (notifications.W001); hardenedreencrypt_email_logswith optimistic locking and batch validation; 10 test cases - Fail-open on unexpected exceptions — Portal middleware
_perform_validationchangedexcept Exception: return Truetoreturn Falseper ADR-0017 — programming bugs must not grant access;PlatformAPIError(network failures) still fails open; login view now seedsactive_customer_idin session after fetching memberships, eliminating a redundant API call on first authenticated request - 8 Semgrep findings resolved —
hashlibwithoutusedforsecurity=False, rawos.path.joinin templates, insecuremark_safeusage; 9 error handling violations fixed (bareexcept, missinglogger.exception); 5 invalidnoqadirectives removed - Domain audit signal crash — fixed 4 wrong attribute names in
DomainsAuditService.log_domain_event:auto_renew_enabled→auto_renew,whois_privacy_enabled→whois_privacy,is_locked→locked,nameservers.all()→nameservers(JSONField, not FK) - Naive datetime warnings in fixtures —
generate_sample_datainvoice/proforma dates now usetimezone.make_aware(datetime.combine(...))instead of barefake.date_between()which produces naive datetimes - CI venv path mismatch — aligned CI and deploy scripts with darwin/linux venv split (
.venv-darwin/on macOS,.venv-linux/in CI) - 2 CI test failures — resolved platform test suite failures from settings/import mismatches
- Fixture
swift→swift_code—generate_sample_databank details key didn't matchvalidate_bank_detailsallowlist, crashingmake fixtures - Double-signal in domain sync tests — removed redundant explicit
sync_domain_to_virtualmin()call (signal already fires ondomain.save()); fixed reactivation test to set domain status in DB before change detection
- API auth hardening: all 11 intentionally public API endpoints now carry
@public_api_endpoint; 3 middleware-only endpoints now have@require_portal_authenticationas backup - Rate limiting consolidation: replaced all 21
@ratelimit(django-ratelimit) decorator usages across 5 view files with custom@rate_limit; CBV dispatch methods now checkrequest.limitedinstead ofresponse.status_code - Settings consolidation:
RATELIMIT_ENABLE+RATELIMIT_ENABLED→ singleRATE_LIMITING_ENABLEDacross all platform and portal settings files, Makefile, ADR-0014, integration tests - Cache setting:
RATELIMIT_USE_CACHE→RATE_LIMIT_CACHE(read by custom decorator) validate_production_secret_key()now acceptssecret_keyas parameter, enforces 50-char minimum, strips whitespace, and has expanded blocklist- Portal
HMACPriceSealerno longer falls back to"insecure-fallback-key"— raisesImproperlyConfiguredifSECRET_KEYis not set - Docker Compose secret vars changed from
:-(weak default) to:?(required/fail-fast) in production files make devCSS deduplication —dev-platform/dev-portalno longer triggerbuild-cssindependently; parent targets (dev,dev-e2e,dev-all) build once- Django-Q2 PID display fixed — merged
qcluster &and$$!into single shell line (each@-prefixed Makefile line runs in its own shell) - Fixture loading noise suppressed —
generate_sample_datatemporarily raisesapps/django-qloggers toWARNINGduring fixture load; errors still surface; only runs inDEBUG=True - Makefile targets renamed —
type-check→check-types; addedtest-fast(failfast + keepdb + parallel) andlint-fix(ruff --fix) targets .gitguardian.yamladded — excludes test fixture files and test directories from secret scanning to prevent false positives on intentional test credentials
- django-ratelimit dependency (
django-ratelimit>=4.1.0) — replaced by custom@rate_limitdecorator; all imports, exception handling (Ratelimited), and mypy overrides removed RATELIMIT_ENABLEsetting (django-ratelimit library kill-switch) — superseded byRATE_LIMITING_ENABLEDRATELIMIT_KEYsetting (django-ratelimit global default key) — each@rate_limitcall specifies its own key
- HKDF key derivation utility (
apps/common/key_derivation.py) — domain-separated cryptographic keys using HKDF-SHA256 (RFC 5869) for NIST SP 800-57 Section 5.2 key separation; 4 domains:mfa-backup,unsubscribe,siem-hash-chain,sensitive-data-hash; optional per-domain env var overrides; 8 unit tests UnsubscribeTokenmodel — opaque UUID-based unsubscribe tokens replacing hash-based tokens that contained email addresses (GDPR Art. 5(1)(c) data minimization); 30-day expiry withconsume()andis_expired()methods;cleanup_unsubscribe_tokensmanagement command; 18 unit tests- Per-server Virtualmin credentials —
api_usernameand_api_password_encryptedfields onServermodel with AES-256-GCM encryption; vault-first, server-model-second credential resolution check_secret_key_usage.pypre-commit hook — blocks new directsettings.SECRET_KEYusage in application code; allowsconfig/settings/and# noqa: SECRET_KEYannotation- Token identity endpoint (
GET /api/users/token/me/) — correct token-auth introspection endpoint; the existingverify_tokenat/api/users/token/verify/requires HMAC customer context (portal-only) and returns 401 for CLI/API consumers; the new endpoint usesTokenAuthenticationonly, returnsemail,staff_role, andtoken_created; 3 tests added - ADR-0031: documents full state of API token authentication, 8 explicit gaps (no expiry, plaintext storage, one-token-per-user, broken verify endpoint), and rationale for
token_infogap-1 fix - Payment model:
updated_at = DateTimeField(auto_now=True)onPayment(migration 0018); aligns with Invoice, Customer, PaymentRetryPolicy TERMINAL_PAYMENT_STATUSESfrozenset constant inpayment_models.py— canonical set of statuses from which payments must not transition; includes bothcancelled/canceledspellingsPayment.apply_gateway_event()method — idempotent gateway status transition withselect_for_update()contract; replaces 3 separate inline implementations- Django system check
portal.W001— deploy-time warning whenIPWARE_TRUSTED_PROXY_LISTis empty in non-debug mode ACCOUNT_LOCKOUT_THRESHOLDsetting (default=1) — makes lockout threshold configurable without code changes- Testing: Cross-service parity test (
test_cross_service_parity.py) preventsretry_after.pydrift between platform and portal services - Testing: Thread-safety test verifies concurrent requests produce unique nonces
- Testing: 11 integration tests for rate-limit flows (login 429, orders 429, retry-after propagation, idempotent retry contract)
- Testing: E2E Playwright tests for rate-limit UX (login throttle, dashboard/catalog under normal load)
- Testing: 4 orders rate-limit unit tests (catalog warning, confirm-payment 429, service re-raise for calculate and create_order)
- BREAKING: Encryption Architecture Consolidation — Consolidated 4 encryption systems into 2, both using AES-256-GCM (NIST SP 800-38D). See ADR-0033.
- Replaced Fernet (AES-128-CBC) with AES-256-GCM for app-level encryption
- Replaced Django Signer (HMAC signing) with real AES-256-GCM encryption for settings, tokens, notifications
- Replaced Fernet with AES-256-GCM in CredentialVault
- Deleted dead
aes256_encryption.pymodule (zero production imports) - Deleted
settings/encryption.py(was signing, not encryption) - New key format: URL-safe base64-encoded 32 random bytes (replaces Fernet key format)
- Unsubscribe URLs no longer contain email addresses — opaque UUID tokens replace
sha256(email:template_key:SECRET_KEY)pattern (GDPR Art. 5(1)(c) data minimization) hash_sensitive_data()uses HKDF-derived key via HMAC-SHA256 instead of bare SHA-256 withsettings.SECRET_KEYand"default-salt"fallback- SIEM
HashChainManageruses domain-specific derived key (siem-hash-chain) instead of rawsettings.SECRET_KEY - Virtualmin credentials vault-only resolution — env var fallback (
VIRTUALMIN_ADMIN_USER/VIRTUALMIN_ADMIN_PASSWORD) removed in non-debug mode - Middleware ordering (prod.py, staging.py) —
PortalServiceHMACMiddlewareandStaffOnlyPlatformMiddlewaremoved afterAuthenticationMiddleware; staff bypass now has access torequest.user - HMAC timestamp validation — allow 2s forward clock skew (
-2 <= delta) for NTP jitter between portal and platform; sharedHMAC_TIMESTAMP_WINDOW_SECONDSconstant extracted toapps.common.constants - HMAC timestamp parsing —
int(float(timestamp))for rolling-deploy backward compatibility - Nonce cache TTL — increased by 30s buffer (
HMAC_TIMESTAMP_WINDOW_SECONDS + 30) to ensure nonces outlive their timestamp validity window - Webhook replay cache key — use full 64-char hex signature instead of truncated
sig[:32] - Portal
_HMAC_TIMESTAMP_RE— tightened from^[0-9]+(?:\.[0-9]+)?$to^[0-9]+$(int-only; platform validates withint()) - Portal rate limiting — atomic
cache.add()/cache.incr()counters replace non-atomiccache.get()+1/cache.set()in bothAuthenticationRateLimitMiddlewareandAPIRateLimitMiddleware - API rate limiter — fail-closed (503) instead of fail-open on cache errors; matches
AuthenticationRateLimitMiddlewarebehavior IPWARE_TRUSTED_PROXY_LIST— renamed fromTRUSTED_PROXY_LISTin portal (request_ip.py, settings, tests) to align with platform setting name- Security scanner
_matches_path_glob()— rewritten usingPurePosixPath.full_match()(Python 3.13); fixes**recursive wildcard matching thatfnmatchdidn't support - Security scanner scoped patterns — all glob patterns now use explicit
**/prefixes for reliable matching - Security scanner XFF regex — fixed extra
"in character class - Security scanner email-in-logs pattern — negative lookahead excludes
_mask_email()and similar safe wrappers - Security scanner ORM
.get()severity — raised from LOW to MEDIUM with concurrent-safety guidance - Stripe webhook processor — refactored to use
Payment.apply_gateway_event()instead of inline status mutation PaymentService.confirm_payment()— uses sharedTERMINAL_PAYMENT_STATUSESconstant instead of inline setobtain_token— usesget_safe_client_ip()instead of rawREMOTE_ADDR_mask_email()— adds null byte (\0) stripping; removes redundant domain-level\n/\rsanitization- Portal API Client: Replaced static
_READ_ONLY_POST_RETRY_ENDPOINTSallowlist with call-siteidempotent=Trueparameter on_make_request— eliminates manual endpoint maintenance and makes retry safety explicit at each call site - Portal API Client: 18+ read-only POST endpoints now pass
idempotent=True(invoices, tickets, services, billing summaries, etc.)
BackupCodeService.hash_backup_code()— weak SHA-256 with SECRET_KEY pepper; production uses Argon2 viaapps.common.encryption.hash_backup_codeBackupCodeService.verify_backup_code()— timing-unsafe==comparison (CWE-208); production uses Django'scheck_password()(constant-time)BackupCodeService.generate_backup_codes()— unused XXXX-XXXX-XXXX format helper; production usesgenerate_codes()(8-digit + Argon2)VIRTUALMIN_ADMIN_USER/VIRTUALMIN_ADMIN_PASSWORDenv var support — credentials must come from vault or per-server encrypted fieldsPaymentService.handle_webhook_payment()and_handle_stripe_payment_intent()— legacy duplicate handlers; Stripe webhook handling consolidated inStripeWebhookProcessor- Associated test classes removed from
test_payment_service.pyandtest_billing_27_todos.py
- CredentialVault RBAC bypass — Permission check was always returning
True; now restricts to staff/superuser/system - Plaintext fallback in e-Factura token storage — Encryption failures now raise instead of silently storing plaintext
- Silent decrypt failure —
decrypt_sensitive_data()now raisesDecryptionErrorinstead of returning empty string - Dict-vs-tuple bug in virtualmin_gateway.py —
get_credential()returns tuple, not dict - Stale object bug in credential rotation —
refresh_from_db()afterstore_credential()prevents overwriting - Docker: Add
ENV PATH="/app/.venv/bin:$PATH"to both Dockerfiles — uv-installed packages (python, gunicorn) were not found at runtime (#12) - Docker: Switch HEALTHCHECK from
python -c "import requests; ..."tocurl -fagainst actual health endpoints (/api/users/health/platform,/status/portal), bumpstart-periodto 60s (#13) - Docker: Fix portal
DJANGO_SETTINGS_MODULEfromconfig.settings(empty__init__.py) toconfig.settings.prodin Dockerfile and all compose files (#14) - Docker: Move
django_extensionsfrombase.pytodev.pywithtry/except ImportErrorguard in both services —--no-devDocker builds don't have it (#21) - Docker: Fix
import_isolation_guard._repo_root()IndexErroronparents[3]in Docker (shorter path depth) withtry/exceptfallback (#26) - Docker: Add
PLATFORM_API_SECRET,PLATFORM_API_ALLOW_INSECURE_HTTP,PLATFORM_TO_PORTAL_WEBHOOK_SECRET,PORTAL_DOMAINto portal environment in all production compose files (#44) - Docker: Fix health check URLs across all compose files and Ansible rollback playbook from nonexistent
/health/to actual endpoints - Retry-After Parser: Capped return value to
MAX_RETRY_AFTER_SECONDS(300s default, configurable viasettings.RETRY_AFTER_MAX_SECONDS) to prevent unbounded server-dictated waits. Addedisascii()guard against Unicode digit bypass,OverflowError/OSErrorhandling for extreme dates, and tighter type annotation (int | float | str | None) - Portal API Client: Added
_safe_parse_jsonhelper to prevent crashes on malformed JSON in 429 responses. Login 429 handler no longer calls unguardedresponse.json() - Portal API Client: Retry loop now fails fast when server's
Retry-Afterexceedsmax_retry_wait_secondscap, preventing futile retries with long backoffs - Portal Services: All billing, services, and tickets service methods now re-raise
PlatformAPIError(is_rate_limited=True)instead of swallowing into fallback returns. Consolidated via_raise_if_rate_limited()helper in each service module - Portal Views: Centralized error handling via
handle_platform_error()helper — eliminates duplicateexcept PlatformAPIError/except Exceptionblocks in billing, services, and tickets views - Portal Views: Removed duplicate toast message from
build_rate_limited_context()— rate-limited views now show inline alert only, no redundant Django message - Portal Dashboard: Per-section rate-limit tracking preserves data from successful API calls when only some sections are rate-limited (e.g., billing 429 doesn't hide tickets)
- Portal Templates: Added
role="alert"andaria-live="polite"to rate-limit inline alert for screen reader accessibility - Portal Auth: Login 429 now shows throttle message ("Too many login attempts, try again in N seconds") instead of silently treating rate-limits as invalid credentials
- Portal Auth:
authenticate_customerre-raisesPlatformAPIError(is_rate_limited=True)instead of swallowing it and returningNone - Portal Auth: Password change view shows rate-limit warning via Django messages framework
- Portal Template: Removed dead
rate_limit_bannerslot frombase.html(context processor setsrate_limited, notrate_limit_banner) - Portal API Client: Thread-safety fix —
_last_request_headersmoved tothreading.local()to prevent cross-thread header contamination on the singleton - Portal Orders: All 4 PlatformAPIError catch sites in services (add_to_cart, calculate, preflight, create_order) now re-raise rate-limited errors instead of swallowing them
- Portal Orders: Views show warning-level rate-limit messages (amber, not red);
confirm_paymentreturns 429 JSON withretry_afterfield - fix(settings): remove duplicate
RATELIMIT_USE_CACHE = "default"in prod.py that overrode the correct conditional assignment - fix(settings): add
RATELIMIT_ENABLE = True/FalsealongsideRATELIMIT_ENABLEDin prod/staging/e2e (both needed: library vs custom middleware) - fix(settings): add
HMAC_SECRETstartup validation to staging.py (was only in prod.py) - fix(settings): align webhook secret default in platform dev.py (
"test-webhook-secret-do-not-use-in-prod") to match portal dev.py - fix(settings): add LocMemCache per-worker limitation docstring in portal base.py
- fix(docs): expand
_is_auth_exemptdocstring explaining exempt path semantics and startswith-to-exact-match rationale - fix(docs): expand
increment_failed_login_attemptsdocstring explaining progressive lockout design - fix(docs): add deprecation comments to
MAX_LOGIN_ATTEMPTS/ACCOUNT_LOCKOUT_DURATION_MINUTESconstants - fix(docs): expand
_verify_platform_webhookdocstring (HMAC-SHA256, replay prevention, cache limitation, serialization contract) - fix(security): wire account lockout into
portal_login_api(#53) — closes the same brute-force gap found inobtain_token; checksis_account_locked(), incrementsfailed_login_attemptson failure, resets on success, uses PII-safe structured logging- 7 regression tests added in
PortalLoginAPILockoutTests(including inactive account and byte-identical response verification across all 4 failure modes) AUTHENTICATION.mdandSECURITY_COMPLIANCE_ASSESSMENT.mdupdated to document lockout enforcement across all 3 credential endpoints
- 7 regression tests added in
- fix(security): atomic lockout counter —
increment_failed_login_attempts()now usesF()expression to prevent lost increments under concurrent requests - docs(security): timing side-channel and HMAC rate limiting documented as accepted risks with rationale in
portal_login_apiandobtain_token - fix(security): harden 9 real vulnerabilities from security audit (confirmed by dual Claude+Codex adversarial review; 33 regression tests + 8 new security linter patterns)
- Critical — token revocation self-revocation pattern:
DELETE /api/users/token/revoke/now usesTokenAuthentication+request.auth; no ownership check needed (#60) - Critical — payment success webhook: HMAC-SHA256 + timestamp + 5-minute replay window using dedicated
PLATFORM_TO_PORTAL_WEBHOOK_SECRET(#49) - High —
obtain_tokenaccount lockout: wireis_account_locked()andincrement_failed_login_attempts(); uniform 401 prevents user enumeration (#53) - High — auth failure log masking:
_mask_email()for PII and control-character sanitization to prevent log injection (#54) - High — IP extraction:
request_ip.pyrewritten usingdjango-ipwarewithTRUSTED_PROXY_LIST; Cloudflare-aware CF-Ray guard; all 4 extraction locations fixed in both platform and portal (#51, #69) - High — rate limiting bypass: replace
os.environ RATELIMIT_ENABLEwithsettings.RATELIMIT_ENABLEDin both platform and portal middleware (#68) - High — Terraform firewall:
firewall_ssh_sourcesandfirewall_webmin_sourcesdefault[]with validation block across all 5 provider modules (#41) - Medium — insecure HTTP startup warning: loud
WARNINGwhenPLATFORM_API_ALLOW_INSECURE_HTTPis active in production settings (#52) - Nuanced — HMAC exempt path check: uses
frozensetexact match instead ofstartswithto eliminate future footgun (#61) - Nuanced —
Payment.confirm_payment(): addsselect_for_update, unique constraint ongateway_txn_id, and row lock inconfirm_orderto prevent double-charge race (#50) - Bonus —
PortalServiceHMACMiddlewareadded to prod and staging middleware stacks (was missing entirely)
- Critical — token revocation self-revocation pattern:
- fix(security): harden HMAC webhook authentication — int timestamps replace floats; future timestamps rejected via
0 <= (now - ts) <= window(prevents preplay); 64-char hex signature format pre-filter; per-process replay deduplication viacache.add(); startup validation forPLATFORM_TO_PORTAL_WEBHOOK_SECRETin both platform and portal prod settings; 8 new webhook tests + 12 structural integration tests - fix(billing): migration 0017 — add
RunPythonstep to convertgateway_txn_id=""→NULLbefore applying the unique constraint; PostgreSQL treats each NULL as distinct so multiple empty strings would fail the migration on production - fix(settings): rename
RATELIMIT_ENABLE→RATELIMIT_ENABLEDine2e.py,prod.py, andstaging.pyto match the key read by middleware; E2E tests were silently not disabling rate limiting due to this mismatch - fix(billing): portal webhook signing in
_send_portal_webhook— compute HMAC-SHA256 of(ts + "." + body)and sendX-Platform-Signature+X-Platform-Timestampheaders; without these, every payment success notification was rejected with 401 - fix(security):
PortalServiceHMACMiddleware— batch hardening (no new dependencies)- Removed
PortalServiceAuthMiddlewaredead code — weak shared-secret auth with no replay protection; regression testtest_legacy_auth_middleware_removedadded to prevent re-introduction - Removed body timestamp cross-check — redundant JSON parse that blocked non-JSON bodies;
body_hashin the canonical string already cryptographically covers any payload timestamp; comment documents this invariant - Rate limiting moved after HMAC validation — previously keyed on the unverified
HTTP_X_PORTAL_IDheader (attacker-controlled); now post-validation, keyed onrequest._portal_id(verified); prevents DoS quota exhaustion via forged portal IDs
- Removed
- fix(security): replace
SessionValidationThrottle(BaseThrottle)with DRF'sScopedRateThrottle— custom implementation used non-atomiccache.get()+cache.set()TOCTOU pattern; replaced withScopedRateThrottle(scope="session_validation")usingDEFAULT_THROTTLE_RATES; zero custom cache logic; rate configurable in settings - fix(security): replace direct
REMOTE_ADDRwithget_safe_client_ip()at all remaining callsites —REMOTE_ADDRis always the immediate TCP peer (the proxy in production); audit logs, rate-limit keys, and security logs recorded the proxy IP instead of the real client; fixed inaudit/signals.py,api/customers/views.py,api/customers/serializers.py,api/users/views.py(×2),common/decorators.py(platform + portal) - fix(security): add scanner pattern for direct
REMOTE_ADDRaccess insecurity_scanner.py— pattern #9 flagsrequest.META.get("REMOTE_ADDR")/request.META["REMOTE_ADDR"]outsiderequest_ip.py; severity MEDIUM; OWASP A09:2021 - fix(settings):
PLATFORM_TO_PORTAL_WEBHOOK_SECRETinportal/config/settings/staging.py— changed from hard fail (ValueError) to optional (WARNINGlog); staging rarely tests the end-to-end payment confirmation flow; comment clarifies this is NOT the Stripe webhook secret — it signs Platform→Portal internal calls only
- Fixed CWE-208 timing side-channel in backup code verification (removed bare
==comparison) - Fixed GDPR Art. 5(1)(c) violation — unsubscribe URLs no longer contain PII (email addresses)
- NIST SP 800-57 Section 5.2 key separation compliance — all HMAC/hash operations use domain-specific HKDF-derived keys instead of raw SECRET_KEY
- NIST SP 800-108 KDF-derived keys —
hash_sensitive_data, SIEM hash chain, and price sealing all use derived or dedicated keys
- 2 regression tests for token revocation:
test_revoke_token_post_rejected_with_405(guards against silent reintroduction of POST pattern) andtest_revoke_token_uses_header_token_not_body(proves body payload is ignored; revokes only the authenticated user's token) - 11 new platform unit tests (
test_api_users_security.py), 8 new portal unit tests (test_portal_security.py), 14 new structural integration tests (test_security_hardening.py) - 8 new security scanner patterns in
security_scanner.py - 7 HMAC middleware tests (
test_hmac_middleware.py) — addedtest_legacy_auth_middleware_removed(regression guard againstPortalServiceAuthMiddlewarere-introduction),test_non_json_body_passes_hmac_validation(proves non-JSON passes after removing cross-check),test_stale_timestamp_rejected; fixed all test nonces to meetHMAC_NONCE_MIN_LENGTH = 32; fixed float timestamps → int; addedLOCMEM_TEST_CACHEoverride to nonce-replay and rate-limit tests (defaultDummyCachesilently no-opscache.add/cache.incr)
AUTHENTICATION.mdupdated: correctobtain_tokenresponse (removed staleis_stafffield), fix revoke verbPOST → DELETE, replace fictional portal JS token-auth code with accurate description of HMAC-signedapi_clientflow; consumer→method table covering all 4 auth pathsAUTHENTICATION.mddual HMAC architecture section added,DEPLOYMENT.mdenv var tables updated,SECURITY_CONFIGURATION.mdandARCHITECTURE.mdupdated with data-flow diagram
All existing encrypted/signed data is invalidated. After deploying:
- Generate new AES-256 keys for
DJANGO_ENCRYPTION_KEYandCREDENTIAL_VAULT_MASTER_KEY - Run
python manage.py migrateto clear old encrypted data - Run
python manage.py post_encryption_upgradeto see re-provisioning status - Users must re-enroll in 2FA
- Re-enter server credentials, API keys, and re-authenticate OAuth integrations
- Portal Design System: Complete component library, design tokens, and living styleguide
- Design tokens (colors, spacing, typography) in Tailwind config
- 10+ reusable template components:
page_header,section_card,modal,toast,badge,empty_state,form_actions,form_error_summary,stat_tile,table - Living styleguide at
/styleguide/(DEBUG-only) with all component variants - Extracted modal and toast JS into static modules
- Bridge template tags for auth form componentization
- Consolidated inline styles into
input.css - Standardized icons (SVG-only, removed emoji from templates)
- Canonicalized mobile header and cookie banner
- QA Tooling: 4 new Makefile targets for design system quality
make lint-templates— Template lint with 9 rules (TMPL001–TMPL009)make check-parity— Platform↔portal template component parity checkmake audit-a11y— Accessibility audit (lang, labels, autofocus, captions)make audit-dark-mode— Dark mode coverage and contrast audit
- Testing: 30+ UI regression tests covering template tags, filters, and components
- 12 new test files: alert, badge, button, card, icon, input, modal, page primitives, SVG policy, toast, design tokens, mobile layouts, XSS sanitization
- Integration tests for template lint rules
- Portal: Normalized billing detail templates with status components
- Portal: Migrated
page_headerandsection_cardacross 20+ templates (billing, tickets, services, MFA, account security, dashboard) - Docs: Added
docs/architecture/ui-ux/portal-design-system.mdas canonical design system specification
- Deployment: Validate
deploy_methodvalue (must begitorrsync) — fails fast on typos likeGITorGitthat would silently skip code deployment - Deployment: Cleared stale
PRAHO_VERSIONdefault in.env.example.prodto force explicit version setting - Tooling: A11Y004 (missing
langon<html>), A11Y007 (autofocus on non-first input) - Tooling: DM005 double-counting fix, TMPL005/008 precision, TMPL001 unquoted fix
- Tooling: Dead code removal (
EXCLUDE_FROM_INPUT_CHECK,DARK_VARIANT,IGNORECASE)
- Deleted
table_enhanced.html(merged intotable.html) - Deleted
portal_mobile_header.html(replaced by canonicalized header) - Deleted 5 stale docs:
portal-ui-ux-backlog.md,phase-b1-pattern-audit.md,8-agent-consolidated-audit.md,setup-initial-data.md,orders.md
- Deployment: Production-grade git-tag deploy pipeline — production always deploys
from an immutable git tag (
PRAHO_VERSIONin.env.prod), with pre-flight validation that the tag exists in the remote repository viagit ls-remote - Deployment: CLI version override for production deploys (
make deploy-prod VERSION=v0.14.0) - Deployment: Configurable staging deploy method — defaults to git HEAD of
DEPLOY_BRANCH, withDEPLOY_METHOD=rsyncas fallback for rapid iteration - Deployment: Deploy summary now shows version + commit SHA for audit trail
- Deployment: DNS pre-flight check verifies FQDNs resolve to target server IP (using
@8.8.8.8) - Deployment:
.envfile as single source of truth — Ansible validates locally, copies to server, and reads values from it (replaces Jinja2env.native.j2template) - Deployment: New
.env.example.prodand.env.example.stagingat project root with documented[REQUIRED]/[OPTIONAL]annotations - Deployment:
setup_initial_datamanagement command for first-deploy bootstrap (categories, settings, scheduled tasks, templates, superuser) - Platform: System status dashboard with integration health checks (database, cache, email, Stripe, e-Factura, scheduled tasks) — HTMX-refreshable partial with staff-only on-demand refresh
- Platform: Django-Q2 scheduled task for daily system status check
- Deployment: Consolidated per-environment Ansible inventory and group_vars into single
native-single-server.ymllayout — environment driven by-e praho_env=staging|prod - Deployment: Renamed Ansible variable
environment→praho_envto avoid reserved keyword collision - Deployment: Merged old
.env.exampleinto.env.example.dev(comprehensive dev reference) - Deployment: Updated
praho_git_repofrom placeholder tocaptainpragmatic/PRAHO.git - Deployment: Rewritten
DEPLOYMENT.mdfor.env-driven workflow with post-deploy integration guide (email, Stripe, e-Factura, 2FA, Sentry) - Portal: Normalized i18n source strings from Romanian to English — translations remain
in
.po/.mofiles; rate limiting now returns redirect + flash message instead of raw JSON for browser form submissions - Platform: SIEM logger derives environment from
DJANGO_SETTINGS_MODULEinstead of hardcoding "production" - Platform: Removed unused
django-storagesdependency and AWS S3 static files config
- Deployment: Production deploys enforce git-only method — rsync blocked for prod
- Deployment: Quoted
.envvalues with spaces for shell sourcing compatibility - Deployment: Tightened
.gitignore—.env.*excludes secrets,!.env.example.*keeps templates tracked; rsync excludes.env.*and.envrcto prevent secret leakage - Deployment: Fixed Caddy log ownership and portal systemd
ReadWritePaths - Deployment: Fixed portal
collectstaticmissingPLATFORM_API_ALLOW_INSECURE_HTTPenv var - Portal: Added
SECURE_PROXY_SSL_HEADERfor TLS-terminated reverse proxy (Caddy/nginx) - Portal: Exempted
/api/fromSECURE_SSL_REDIRECT(localhost inter-service communication) - Portal: Added
/robots.txtto public path exemptions in portal middleware - Platform: Exempted
/api/fromSECURE_SSL_REDIRECTin prod settings - Platform: Fixed missing required env vars in logging configuration test
- Orders: Fixed fail-closed validation and consistent product filtering; accept
product_slugas fallback identifier in cart and order API - Security: Env-driven host config with split domains, native deployment hardening
- Deleted per-environment Ansible files:
group_vars/{dev,prod,staging}.yml,inventory/{prod,staging,single-server,staging-single-server}.yml - Deleted
deploy/ansible/roles/praho-native/templates/env.native.j2(replaced by.envcopy) - Deleted
deploy/.env.staging.example(replaced by.env.example.stagingat project root) - Deleted
.env.example(replaced by.env.example.dev) - Removed
django-storagesfrom dependencies
Comprehensive ruff auto-fix pass across all 21 platform apps plus scripts and config.
Introduces outbound_http.py module for secure HTTP client defaults (TLS verification,
connection pooling, timeout enforcement) used by all external-facing services.
- Ruff formatting fixes, import ordering, and unused import removal across all apps
- MyPy type annotation improvements: added return types, parameter types,
ClassVarannotations - New
outbound_http.pyshared module (platform + portal) withSecureHTTPSessionbase class - New mypy override for
outbound_httpmodule (untypedrequests.HTTPAdapter.init_poolmanagerstubs) - Database migrations:
billing/0016_alter_oauthtoken_options,infrastructure/0005_alter_driftremediationrequest_status - 7 new outbound HTTP transport tests (SIEM, domains, notifications, provisioning, common)
Resolved 3 root causes responsible for the majority of the 52 E2E test failures (52 → 8):
Portal ticket views passed raw API response dicts to Django templates. The |date filter
only works on datetime objects, and date_created (a fallback key) didn't exist in the
API response, causing VariableDoesNotExist in Django 5.2 DEBUG mode → HTTP 500.
- Extracted
DictAsObjfromdashboard/views.pyto new sharedportal/apps/common/api_utils.py— wraps API dicts in objects with dot notation and auto-parses ISO 8601created_at/updated_atstrings into timezone-awaredatetimeobjects - Wrapped ticket dicts in
DictAsObjinticket_list(),ticket_search_api(), andtickets_dashboard_widget()(portal/apps/tickets/views.py) - Simplified template date expressions from 5-fallback chains to
{{ ticket.created_at|date:"d.m.Y"|default:"—" }}(tickets_table.htmllines 96, 162)
Three templates referenced {% url 'orders:product_catalog' %} but orders/urls.py defines
the name as catalog. The NoReverseMatch error crashed cart empty states, invoice search,
and service search — cascading into cart/checkout/order flow tests.
- Fixed URL name in
cart_empty.html,invoices_table.html,services_table.html
Cart error handlers (add_to_cart, update_cart_item, remove_from_cart) returned
error_message.html (id="error-notification") but HTMX outerHTML swap expected elements
matching the target ID (#cart-widget or #cart-totals). The ID mismatch caused
htmx:targetError console errors caught by ComprehensivePageMonitor.
add_to_carterrors now returncart_error_notification.html(id="cart-widget")update_cart_itemandremove_from_carterrors now returncart_empty.html(id="cart-totals")calculate_totals_htmxerrors already returncart_empty.html(fixed in prior session)- Added
#cart-widgetplaceholder toproduct_detail.html(was missing —hx-targethad no match) - Error responses now return HTTP 422 with
HX-Retarget/HX-Reswapheaders so HTMX routes errors to#cart-notificationsandevent.detail.successfulcorrectly returns false
e2e-admin@test.local had no CustomerMembership record because generate_sample_data.py
(the command run by make fixtures) created the user but never linked it to a customer.
Portal login requires customer_id from the Platform API, which queries is_primary=True
membership — returning None caused session middleware to reject the request.
- Added
CustomerMembership.update_or_create()for e2e-admin ingenerate_sample_data.py - Also fixed
setup_test_data.py:get_or_create→update_or_createsois_primary=Trueis enforced on re-runs (thedefaultsdict only applies on CREATE, not GET)
test_product_pricing_management: Now reads available currencies from the actual<select>dropdown instead of assuming USD/EUR/RON exist. Skips gracefully if all are taken.test_staff_complete_billing_workflow: Graceful skip when proforma is not in convertible statetest_ticket_isolation: Allow HTTP 200 when server redirects away from nonexistent ticket (Django returns 200 on/tickets/list, not 403/404 on the fake ticket URL)product_detail.html(platform): Addedfor/idattributes to quantity input for a11y
Portal QA walkthrough identified 19 findings; red-team review elevated 3 more from "not a bug" to confirmed. All fixes include unit + E2E test coverage.
- C1: Cart checkout blocked — Portal stored
product_slugbut Platform expected UUIDproduct_id. Fix: store UUID on cart add + Platform slug fallback lookup (orders/services.py,api/orders/views.py) - C2: Registration blocked —
terms_acceptedcheckbox missing from template and API payload (register.html,forms.py)
- H1: Invoice detail —
invoice.bill_to.name→invoice.bill_to_name(flat field); addedstatus_displayproperty to Invoice schema with i18n labels (invoice_detail.html,schemas.py) - H2: Login error invisible — view used
messages.error()but template checkedform.non_field_errors; changed toform.add_error(None, ...)for inline display (users/views.py:285) - M7: TOTP setup "Failed to initialize" — Portal sent only
customer_id, Platform's@require_customer_authenticationalso requiresuser_idin HMAC body (api_client/services.py,users/views.py)
- M1: Company profile "Not specified" — template checked
company_data.namebut view setcompany_data.company_name(company_profile.html:80) - M2: Profile "Last Login: Never" / "Member Since: N/A" —
CustomerProfileSerializer.to_representation()now includeslast_loginanddate_joined(customers/serializers.py) - L5: Proforma VAT rate showed "0.2%" instead of "21%" — new
as_percentagetemplate filter converts proportion to percentage (formatting.py,proforma_detail.html) - M3/M4: Service detail "Calculating..." —
{% if service.service_age_days %}→{% if service.service_age_days != None %}(0 is falsy); fixtures now setactivated_at/expires_at - L3: Currency formatting — services templates used
floatformat:2+ hardcoded "RON"; now use{{ service.currency_code|default:"RON" }}
- M9: Product fixtures now include descriptions and all 3 billing period prices
- L4: Ticket fixture titles no longer embed
[OPEN]/[IN_PROGRESS]status prefix (generate_sample_data.py) - L2: Billing cycle dropdown labels shortened to prevent truncation
- Empty states: billing, services, and cart now have CTAs ("Browse Hosting Plans", "Browse Products") instead of dead-end text
- Tickets mobile breakpoint gap fixed (
sm:hidden→md:hidden) — tablet users no longer see blank page - Breadcrumb component colors fixed for dark theme (was invisible:
text-slate-900onbg-slate-900) - "Logout" nav text now translated (
{% trans %}) - Ticket create validation:
alert()replaced with inline error banner - MFA TOTP setup: added
{% if form.errors %}block for failed verification - Company edit form: added
non_field_errorsdisplay - Toast auto-dismiss raised from 3s to 5s (WCAG recommendation)
- Ticket detail: "Back to Tickets" link moved from bottom to top of page
- Ticket dates: replaced fragile string-slicing with Django
|date:"d.m.Y"filter - Scroll hint arrows: initial
opacity:0prevents flash on page load - Company edit: removed redundant
min-h-screen bg-slate-900wrapper - Removed dead Alpine.js loading overlay from base template
- Removed conflicting HTMX global JS indicator listener (CSS handles it)
- Account status stat card driven from context (was hardcoded "Active")
make lint-security reported 9 blocking semgrep findings. All resolved with real code fixes where viable, # nosemgrep suppression only for confirmed false positives.
common/utils.py(direct-use-of-httpresponse): Addedcontent_type="text/plain"to maintenance mode response — browser won't parse as HTML, eliminating XSS vectorportal/decorators.py(direct-use-of-httpresponse×4): Addedcontent_type="text/plain"to all 4HttpResponseForbiddencalls — hardcodedgettext_lazystrings with no user inputprovider_config.py(non-literal-import): Replacedimportlib.import_module()with eager registration pattern (register_sync_fn()+_SYNC_FN_REGISTRY). Functions register at import time → no runtime dynamic import surface. Cascaded toprovider_sync.py,apps.py,sync_providers.pymanagement command, and 2 test filesplans_list.html(blocktranslate-no-escape): Wrapped{% blocktrans %}in{% filter force_escape %}
Root cause: semgrep requires # nosemgrep on the first line of the match, not the closing ). Ruff's formatter had moved comments to closing lines where semgrep ignores them.
validation_service.py(dynamic-urllib-use): URL is hardcodedhttps://{ip}:10000/— not user-controlledvirtualmin_gateway.py(request-session-with-http):session.mount("http://")is adapter registration, not an HTTP requestvirtualmin_service.py,users/models.py,setup_test_data.py(unvalidated-password×3): System-generated random password, Django convention (form-level validation), and test fixture respectivelyformatting.py(avoid-mark-safe):mark_safe(escape(text))— standard Django pattern, content explicitly escapedportal/users/views.py(request-post-after-is-valid): Reads redirect URL param validated byurl_has_allowed_host_and_scheme()product_detail.html(translate-as-no-escape):{% trans %}output auto-escaped by Django template engine
test_input_validation_and_sanitization:add_to_cartview silently returned 200 for invalid input (XSS slug, non-integer quantity) becauseValueErrorwas caught by broadexcept Exception. Fix: addedOrderInputValidator.validate_quantity()andvalidate_billing_period()before cart processing, returns 400 on invalid input. HTMX endpoints should return 4xx for bad input — not mask errors as 200test_signature_comparison_statistical_analysis: HMAC timing test was flaky in Docker (CV=5.2 vs threshold 3.0). Fix: IQR (Interquartile Range) outlier filtering before computing coefficient of variation, relaxed thresholds to accommodate kernel scheduling noise on non-RTOS systems. Added comprehensive docstring explaining what the test can and cannot prove
- Server-side log checking in E2E tests (ADR-0028): ComprehensivePageMonitor now detects Django errors in platform/portal logs, correlated per-test via X-Request-ID
- Multi-provider cloud gateway: AWS (
aws_service.py), DigitalOcean (digitalocean_service.py), and Vultr (vultr_service.py) implementations ofCloudProviderGatewayABC alongside existing Hetzner - Config drift detection (ADR-0029):
DriftCheck,DriftReport,DriftSnapshot,DriftRemediationRequestmodels withdrift_scanmanagement command (exit codes: 0=clean, 1=drifts, 2=errors, 3=both) - Infrastructure management commands:
deploy_node,manage_node,cleanup_deployments,store_credentials— CLI parity with web UI for all deployment lifecycle operations - Drift remediation UI: Templates and views for drift dashboard, scan results, and remediation approval workflow
- E2E test suite cleanup: Deleted 3 legacy duplicate test files (-1,003 lines), migrated ~127 tests to use
monitored_staff_page/monitored_customer_pagefixtures, standardized imports fromtests.e2e.utilstotests.e2e.helpers - ComprehensivePageMonitor bug fixes:
add_expected_error_patternsnow accumulates across multiple markers (was overwriting), non-string args coerced tostr, Playwright response listener properly unregistered in__exit__
8-agent deep audit identified 50 issues (10 Critical, 18 High, 16 Medium, 6 Low). All fixed with 221 new tests achieving 90%+ coverage on changed code. 5,264 platform tests pass.
- C1:
virtualmin_disaster_recovery.py—VirtualminGatewaynow receivesVirtualminConfiginstead of raw server object - C2:
deploy_nodecommand —--dry-runexits beforedeployment.save(), preventing phantom DB records - C3: Cloudflare API token removed from all queue function signatures; fetched from
SettingsServiceat task execution time (prevents cleartext in Django-Q2 task table) - C4:
tasks.py— Fixedtimezone.make_aware()on already-aware datetime in cost calculation - C5:
DriftCheck.started_atchanged todefault=timezone.now(wasNone) - C6:
provider_sync.py—AWS_REGION_CODE_MAPreplaces naive AZ-name truncation for region codes - C7:
hcloud_service.py—int(server_id)moved inside try/except in all 8 methods (was crashing on invalid IDs) - C8: Vultr power-ops test mocks fixed to return valid instance data with correct status normalization
- C9:
aws_service.py— Idempotency tag changed from"deployment-id"to"praho-deployment"(matches other providers) - C10: Deployment status template —
progress_stepcomputed in view, stages passed as list (was splitting comma-string in template)
- H1:
views.py—async_task()call moved insidetransaction.atomic()for drift remediation approval (TOCTOU prevention) - H2:
deployment_service.py— DistinguishedErr(transient failure) fromOk(None)(server gone) in deploy retry; only clearsexternal_node_idon confirmed absence - H3:
drift_scancommand — Three-code exit scheme (0/1/2/3) for clean/drifts/errors/both - H4:
retry_deploymentusestransition_to("pending")instead of directstatus = "pending" - H5:
stop_node/start_nodeusetransition_to()with new transitions:completed → stopped,stopped → completed - H6:
destroy_nodewrapped intransaction.atomic()+select_for_update()for TOCTOU prevention - H7:
can_be_destroyedproperty now includes"stopped"status - H8:
is_err()check beforeunwrap()on master SSH public key (was crashing on vault errors) - H9:
aws_service.py:get_servercatches genericExceptionafterClientError - H10:
hcloud_service.py— Server status normalized vianormalize_server_status() - H11:
vultr_service.py—delete_firewallreturnsOk(True)on 404 (idempotent) - H12:
aws_service.py—upload_ssh_keychecks fingerprint before delete-and-recreate - H13:
digitalocean_service.py—DO_REGION_COUNTRY_MAPdict replaces brokenslug[:3].upper()for country codes - H14:
manage_nodecommand — Status validation before async dispatch (mirrors view precondition checks) - H15:
cleanup_deploymentscommand — Only marks"destroyed"when cloud deletion succeeds - H16:
virtualmin_auth_manager.py— Dict-dispatch pattern with explicitErrfor unknown auth methods - H17:
_mark_failed()acceptsaudit_ctxparameter for audit trail threading - H18: Portal HMAC tests assert
customer_idextraction from response
- M1/M4:
get_next_node_number—IntegrityErrorretry for empty-table race condition - M2:
views.py—distinct=TrueinCount()for drift dashboard annotations - M3: (Covered by C7 — same
int()fix) - M5: Provider sync uses Vultr public API for plan catalog (not private
_request) - M6: Provider config tests updated for vault-first credential lookup
- M7: DigitalOcean provider code changed from
"do"to"dgo"(3-char convention) - M9:
_PROVIDER_REGISTRYandnormalize_server_statusdocumented with docstrings - M10:
vultr_service.py—create_servervalidates image is non-empty before API call - M11:
digitalocean_service.py—logger.warning()for SSH keys not found during resolution - M13: Canonical status vocabulary documented on
normalize_server_status - M14: ADR-0028 updated to document
NO_REQUEST_IDexclusion by design - M15: Quota restoration uses
is not Nonechecks instead of truthy (zero quota = unlimited) - M16: Migration 0004 —
started_atdefault changed fromNonetotimezone.now
- L1: Replaced realistic AWS key literals with obviously fake test values
- L4:
apps.py— Narrowedsuppress(Exception)tosuppress(ImportError)for django_q import - L5: Fixed
/home/claude/...paths in docs to relative paths
- 12 vacuous E2E tests hardened: Workflow tests that silently passed on failure now
pytest.fail(), security isolation tests now assert denial, soft-check helpers now return bools with caller assertions - Reply textarea selector: Portal ticket tests now use correct
name="message"field - Duplicate proforma helpers consolidated: Merged
_fill_proforma_formand_fill_workflow_proforma_forminto single function withsubmitparameter
- hcloud Python SDK integration (ADR-0027): Replace Terraform with typed Python SDK for Hetzner Cloud server provisioning
- Provider catalog sync: Live sync of regions, server types, and pricing from Hetzner API via
sync_providersmanagement command - Sync providers UI: "Sync Providers" button on Cloud Providers page with provider-agnostic dispatch
- First-boot provider sync: Automatic catalog sync on first migration when no providers exist
- Periodic provider sync: Daily 4:00 AM background task for catalog updates
max_domainsfield: Configurable per-deployment domain limit with size-based defaults (25-500)- Credential vault for cloud providers: Provider API tokens stored via encrypted
CredentialVaultwithcloud_providerservice type; env-var fallback for bootstrap only - Provider-agnostic sync registry:
PROVIDER_SYNC_REGISTRYandget_provider_sync_fndecouple sync dispatch from hard-coded Hetzner references - Infrastructure audit trail:
InfrastructureAuditServicewired into deployment lifecycle (start, complete, fail, retry, destroy) and provider CRUD (create, update, region toggle) - Deployment state machine —
stoppedstate: New intermediate state betweencompletedanddestroying/failedwith defined transitions - Makefile
check-envguard: Fails fast with clear message when.envis missing; wired as prerequisite to all dev-server targets - Customer profile sub-form templates:
address_form.html,billing_profile_form.html,note_form.html,tax_profile_form.htmlfor inline editing - Portal Frontend Architecture (ADR-0026): Unified list page design system for Tickets, Invoices, and Services portal pages
- Shared template components:
list_page_header.html,list_page_filters.html,list_page_skeleton.html— composable includes for consistent list page layout - Shared pagination utility:
apps.common.pagination.PaginatorDataandbuild_pagination_paramsreplace ~20 lines of duplicated pagination math per view - Invoices search endpoint:
invoices_search_apiHTMX endpoint with live search by document number - Services search endpoint:
service_search_apiHTMX endpoint with client-side search by service name/domain - Tab-based filtering: All 3 portal list pages use HTMX-powered tab navigation for primary filter dimension
- SVG icon template tag system:
{% icon "name" %}replaces inline emoji characters across all templates - Account health banner: Persistent portal banner with session-cached account summaries
- 503 maintenance template: Security-clean error page for Semgrep compliance
- Staff customer management E2E tests: Playwright tests for customer list, detail, create/edit, profile sub-forms, and access control
- Deployment pipeline: Replaced 4 Terraform stages (config gen, init, plan, apply) with single hcloud SDK call
- Provider config: Removed Terraform-specific keys;
get_credentials_for_providerrenamed toget_provider_tokenreturningResult[str, str] - Deployment state machine:
transition_tonow raisesValidationErrorinstead of returning bool; redundant.save()calls removed .envloading: Movedload_dotenvfrommanage.pyintodev.pysettings (both services) so WSGI/ASGI workers also load.env- Provider sync pricing: Extracted testable
_extract_pricinghelper preferringfsn1with fallback for ARM server types - Tickets list page: Refactored from ~202 lines to ~42 lines using shared includes; status filtering changed from dropdown to tabs
- Invoices list page: Refactored from ~347 lines to ~42 lines using shared includes with HTMX live filtering
- Services list page: Refactored from ~343 lines to ~42 lines using shared includes with HTMX tabs
- Portal ticket page title: Shortened from "Support Tickets" / "My Support Tickets" to "Tickets"
- Ticket-service linking: Tickets can now be linked to provisioned services;
TicketAPIClientrenamed for consistency - Makefile: Added
sync_providersstep tofixturesandfixtures-lighttargets
- Deployment URL patterns: Changed
<uuid:pk>to<int:pk>(NodeDeployment usesBigAutoField, not UUID) - Customer security: Replaced
@login_requiredwith@staff_requiredon all user management views; added_get_accessible_customerACL check - Customer delete confirmation: Server-side validation that typed name matches actual customer name before soft-delete
- Product price authorization: Added
@admin_requiredtoproduct_price_editandproduct_price_deleteviews - HTMX CSRF headers: Added
hx-headers='{"X-CSRFToken": ...}'to all product toggle buttons (active, public, featured) - Product pricing display: Switched from
prices_by_currencygrouped dict toactive_pricesflat list - Template fixes: Removed corrupted HTML in
customers/form.htmlheading; fixedblocktransvariable references in customer list pagination; added{% load i18n %}to deployment status partial - Provider list count: Added
distinct=Trueto deployment count annotations to fix over-counting - E2E test selectors: Updated 5 test files to match SVG icon system (removed emoji from Playwright
:has-text()selectors); updated proforma form field names fromlines-0-*toline_0_* - Invoices search: Search input on invoices page was never wired to backend — now filters by document number via HTMX
- Billing portal invoice view: Use
request.user.idinstead of.pk - Staff ticket replies: Allow staff reply on closed tickets; add
inertto mobile nav to prevent form conflicts - VAT rate: Replace hardcoded 19% VAT with dynamic
TaxServicelookup (21% since Aug 2025) - Billing PDF exports: Correct parameter order in PDF export views
- Order item audit: Wrap order item audit call in
BusinessEventData
- Terraform fields: Removed
terraform_state_pathandterraform_state_backendfrom NodeDeployment model - Terraform dependency for Hetzner: No longer required for server provisioning (kept deprecated for other providers)
- e-Factura integration: Real ANAF API submission (
submit_invoice), status polling (check_status), and XML download (download_xml) viaEFacturaClient; simulated fallback in DEBUG mode when credentials are not configured - Payment gateway:
create_customer,charge, andcreate_subscriptionmethods onBasePaymentGatewayandStripeGateway;PaymentService.process_subscription_paymentorchestrates gateway customer creation, Stripe charge, and subscription record persistence - Subscription billing cycle:
PaymentService.run_billing_cyclequeries active subscriptions due for billing, processes each payment, applies dunning rules for failures, and updates service statuses - Refund processing:
RefundService.process_refundcalls gateway refund, records the transaction, and updates invoice/order status; wired intoinvoice_refundandapi_process_refundviews - Invoice payment tracking:
Invoice.record_paymentupdatespaid_cents,status, andpaid_at; status transitions enforced (draft→sent→partial→paid) - Proforma PDF & email:
ProformaService.generate_pdfrenders via WeasyPrint;send_proforma_emaildispatches bilingual notification with PDF attachment - Credit note generation:
billing.signalsgenerates a credit noteInvoice(kind=credit_note) when an order refund signal fires - Invoice numbering: Sequential
BillingService.get_next_invoice_numberwithYYYYMMDD-NNNNformat - Proforma→Invoice conversion:
BillingService.convert_proforma_to_invoicecopies lines and marks proforma as converted - Payment retry & dunning:
BillingService.retry_failed_paymentwith exponential backoff;tasks.process_dunningescalates throughwarn → retry → suspend → cancelstages - Billing tasks:
submit_invoice_to_efactura,send_payment_reminders,cancel_payment_reminders,validate_vat_number(ANAF/VIES),process_auto_payments(Stripe auto-charge) - Metering alerts:
UsageAlertService._send_alert_emailsends real notification viaEmailService - Order editing:
order_editview processes form POST with line-item updates - Customer services API:
customer_servicesendpoint returns actualServicequeryset - Ticket stats API: Manual average response time calculation (replaces broken SQLite
Avgon datetime) and satisfaction rating aggregation
- Test isolation: Switch default test cache from
LocMemCachetoDummyCache; addLOCMEM_TEST_CACHEconstant for tests that explicitly exercise cache behavior, applied via@override_settingsto ~18 test classes across 13 files - TransactionTestCase fixtures: Replace
Currency.objects.create()withget_or_create()in 4 TransactionTestCase files (21 occurrences) to preventIntegrityErrorunder--parallel - Cost service singleton: Reset
_instanceintearDownwhereCostServiceis tested as a singleton to prevent state leakage between parallel workers - Test settings: Remove stale
DJANGO_TEST_PROCESSES=1override that prevented parallel execution; clean up unused imports inconfig/settings/test.py - Makefile: Add
make test-file FILE=<dotted.path>target for running a single test module - Pre-commit: Disable
check-executables-have-shebangsin Docker (VirtioFS marks all files +x); fix i18n linterexcludepattern to correctly skiptests/andscripts/directories
- Security Hardening: Harden templates against XSS across platform and portal services, fix CSRF and security decorator issues in views
- Portal Auth: Add ADR-0017 documenting portal auth fail-open strategy
- E2E Stability: Add
NORELOAD=1support to dev targets for E2E reliability, harden billing and services test selectors
- Dependencies: Remove legacy
requirements.txtfiles — all dependencies now managed via uv workspace - Docker: Update Dockerfiles and Makefile for uv-only dependency management
- Billing Tests: Expand portal billing test coverage
- HMAC Test Helpers: Shared
HMACTestMixinfor portal-to-platform API tests (tests/helpers/hmac.py)
- Security Hardening: Applied
@secure_user_registration()and@secure_invitation_system()decorators toUserService— enforces privilege escalation prevention (stripsis_staff/is_superuserfrom user data), XSS sanitization (strip_tagson name fields), cache-based rate limiting, and role validation against allowed roles - Audit → Notification Integration: Critical audit alerts and file integrity alerts now trigger admin email notifications via
NotificationService.send_admin_alert(), gated bySettingsServicetoggles (audit.notify_on_critical_alerts,audit.notify_on_file_integrity_alerts) - Settings Import Endpoint: New
POST /settings/api/import/endpoint accepting JSON body or multipart file upload — validates keys againstDEFAULT_SETTINGS, skips sensitive settings unless?include_sensitive=true, logs imports vialog_security_event, protected by@admin_required - Customer Analytics:
update_customer_analyticstask now queries real data —total_ordersfrom Order count,total_revenuefrom paid Invoice aggregation,engagement_scorefrom weighted formula (order frequency 40%, login recency 30%, ticket activity 30%) with configurable weights viaSettingsService - Metering Threshold Enforcement:
_take_threshold_actioninUsageAlertServicenow executes real enforcement —throttle/suspendcallProvisioningService.suspend_services_for_customer(),block_newsets a 24h cache flag, all actions audit-logged - E2E Portal Test Suite: Comprehensive Playwright E2E tests for customer services (detail views, plans, action requests, usage stats), billing (invoice sync, filtering), users (team management, invitations, roles, access control), tickets (creation, replies, search/filter), dashboard (widgets, responsive layout), navigation (sidebar, breadcrumbs, mobile), and signup/order flows
- Makefile
dev-e2e-bg: Backgrounded dev server target that starts both services with rate limiting disabled, waits for readiness, and returns — suitable for CI pipelines
- README Badges: Added PostgreSQL, Tailwind CSS, GDPR compliance, and test count (4,000+) badges; added mypy strict and Ruff lint quality badges; removed DCO badge (PR-only workflow)
- CI Coverage: Switched from Codecov to gist-based dynamic badge for coverage reporting, then removed Codecov integration entirely
- Portal Billing Template: Added invoice sync button (desktop + mobile responsive) to
invoices_list.htmlusing HTMXhx-postwith CSRF token
- Audit Coverage: 6 new test files — compliance reporting, SIEM integration, logging formatters, management commands, services coverage, views coverage (692 tests)
- Billing Coverage: 11 new test files — views, signals, tasks, e-Factura, invoices, payments, refunds, subscriptions, metering gateway, misc coverage (1,165 tests)
- Security Tests: Updated 6 placeholder assertions in
test_enhanced_validation.pyfromis_ok()to properis_err()failure checks
- 🔒 Semgrep Full Triage & Remediation: Triaged and resolved all 192 Semgrep findings (15 true positives, 73 defense-in-depth, 104 false positives)
- True Positives Fixed (15):
- CRITICAL: Stored XSS via
user.first_namein{% blocktranslate %}— wrapped with{% filter force_escape %} - HIGH: Open redirect in customer switch views — validated with
url_has_allowed_host_and_scheme() - HIGH: Unauthenticated
resource_allocation_webhook— added HMAC validation - MEDIUM: Stored XSS via
search_queryin customer list — wrapped with{% filter force_escape %} - MEDIUM: DOM XSS via
innerHTMLinshowNotification()— replaced withtextContent - MEDIUM: Unescaped f-strings in Virtualmin HTMX responses (5 locations) — replaced with
format_html() - MEDIUM: HttpResponse XSS in ticket views — replaced with
format_html() - LOW: Missing
validate_password()in password reset serializer — added Django password policy enforcement
- CRITICAL: Stored XSS via
- Defense-in-Depth Hardening (73):
- Wrapped all
{% blocktranslate %}blocks across 18 template files with{% filter force_escape %}(excluding plain-text email templates) - Added
secure=request.is_secure(),httponly=True,samesite="Lax"to language and consent cookies - Added DRF anonymous rate throttling (60/min) to Portal REST endpoints
- Removed redundant Alpine.js CDN tag from
service_detail.html(already loaded from base.html) - Added
|escapejsfilter to JS-interpolated domain name indomain_renew.html
- Wrapped all
- False Positive Suppression (104): Added
nosemgrepcomments with justifications- 34
template-translate-as-no-escape— output already escaped via|escapejsfilter - 14
avoid-mark-safe— content sanitized by bleach/escape beforemark_safe - 8
unvalidated-password— test data generation andUserManager(not user-facing) - 6
direct-use-of-httpresponse— string literals and developer-configured integers - 6
django-no-csrf-token— CSRF token present on adjacent line (parser limitation) - 5
no-csrf-exempt— HMAC-authenticated inter-service endpoints - Remaining misc: plain-text email templates, admin-managed URLs, Stripe SRI limitation, internal network HTTP
- 34
- True Positives Fixed (15):
- 🔒 PRAHO Architectural Security Scanner: 18 custom rules detecting PRAHO-specific vulnerabilities
- Rules PRAHO-001 through PRAHO-018 covering: missing middleware, HMAC secret fallback, AllowAny on destructive endpoints, fail-open middleware, unprotected billing views, SSL verification, CSP misconfiguration, and more
- AST-based detection for complex patterns (decorator analysis, class scope tracking, inline auth recognition)
- Inline suppression support via
# praho-security: ignore[RULE-ID] - Wired into
make lint-securityas third scanning phase - 22 unit tests with 100% rule coverage
- 🔒 Legacy HMAC Removal: Eliminated all legacy pipe-delimited HMAC canonical format code
- Removed
_should_use_legacy_canonical(),_prepare_legacy_request_headers(), and legacy retry block from Portal API client - Modern newline-separated format with body hash is now the only HMAC implementation
- Removed
- 🔒 ADR-0017 Portal Auth Fail-Open Strategy: Documented intentional fail-open behavior in Portal authentication middleware
- Critical path comments explaining why Portal fails open during Platform API outages (stateless service cannot fail closed)
- 5 safeguards: 6h hard TTL, no metadata update on failure, independent session security, error type split, thundering herd protection
- Scanner suppression with
# praho-security: ignore[PRAHO-006]
- 🔒 CVE Patches: Patched hardcoded secrets and removed sensitive defaults from non-dev settings
- 📦 Complete uv Migration: Fully migrated package management from pip/requirements.txt to uv
make installnow runsuv sync --all-groupsinstead ofpip install -r requirements/*.txt- Dockerfiles use
COPY --from=ghcr.io/astral-sh/uv:latestwithuv sync --frozenfor reproducible builds - All 4 GitHub Actions workflows migrated to
astral-sh/setup-uv@v4 - Added
semgrep>=1.56.0to dev dependency group - Deleted 8 legacy requirements files
- Updated 5 documentation files with uv commands
- 🔧 Ruff Bug Fixes: Fixed pre-existing code quality issues
- Fixed undefined variable
ticket_numberin API ticket views (F821) - Fixed bare
exceptin customer serializer (E722) - Removed unused import
Userin customer API views (F811) - Removed unused variable assignments in portal conftest and ticket views (F841)
- Fixed undefined variable
- Full i18n Coverage: 100% Romanian translations for Platform (4,470 entries) and Portal (1,285 entries) — wrapped all hardcoded Python strings (
ValidationError,help_text,verbose_name,short_description,choices) and template strings (alert(),{% button %}) with_()/{% trans %} - i18n Linter (
scripts/lint_i18n_coverage.py): AST-based linter detecting unwrapped i18n strings (7 Python checks + 3 template checks), integrated intomake lintPhase 4 and pre-commit - Translation Tooling (
scripts/translate_po.py): Dictionary engine (500+ Romanian terms), Claude AI mode (--claude), YAML review workflow (generate → review → approve → apply), per-app coverage stats - Makefile i18n Targets:
make translate,make translate-ai,make translate-apply,make translate-stats,make i18n-extract,make i18n-compile - i18n Allowlist (
scripts/i18n_coverage_allowlist.txt): Suppression file for programmatic strings that are not user-facing (filter tuples, seed data,unique_togetherconstraints);--allowlistflag wired into alllint_i18n_coverage.pyinvocations in Makefile and pre-commit hook - CI: Automated GitHub Release creation from annotated tags
- Subscription Resume Bug: Fixed
Subscription.resume()clearingpaused_atbefore calculating paused duration, which caused subscriptions to not extendcurrent_period_endandnext_billing_dateby the time spent paused - Legal Views DateTime: Replaced
timezone.datetime(..., tzinfo=timezone.utc)with stdlibdatetime(..., tzinfo=UTC)in legal views — the Djangotimezonemodule has nodatetimeconstructor, so the previous code was using a re-export that could break across Django versions - WebAuthn Model Registration: Fixed
signals.E001system check error —WebAuthnCredentialmodel (defined inmfa.py) was not discoverable by Django's model registry; now imported inUsersConfig.ready() - e-Factura XML Tax Fallback: Fixed
or-based tax amount fallback that treated0as falsy — replaced with explicitNone-check so zero-tax invoices generate correct XML - Portal Page Param Parsing: Added
try/exceptaroundint()conversion of page query parameters in billing, tickets, and services views to prevent 500 errors on malformed input
- MyPy Type Safety Cleanup: Removed 178 redundant
# type: ignorecomments across 75 files, fixed real type bugs (any→Any, missing imports, incorrect return types), removed dead code, and audited all remaining type suppressions to use specific error codes ([arg-type],[assignment], etc.) instead of bare# type: ignore - Incremental Type-Check Hook: Rewrote
check_types_modified.pyto use a ratchet pattern — compares mypy error counts against the merge-base and only fails if new errors are introduced, allowing the hook to work on codebases with pre-existing type errors - Test Passwords: Standardized test passwords to
testpass123across all test suites
- Security Scanner:
scripts/security_scanner.py— AST-based static security scanner covering OWASP Top 10 categories; detects hardcoded secrets, dangerous function calls (eval/exec with dynamic args, pickle.loads), SQL injection patterns, and insecure subprocess usage; integrates pip-audit/safety for dependency vulnerability scanning; supports JSON and console output modes with configurable severity thresholds; invokable standalone or viamake lint-security - Architecture Diagrams: New
docs/architecture/directory with seven Mermaid diagram files — system overview, entity relationships, data flow, deployment topology, and app dependencies; accompanied byREADME.md(diagram index and render instructions) andCHANGELOG.md(diagram history) - Documentation Updates:
README.mdanddocs/ARCHITECTURE.mdupdated to reflect current two-service architecture (Platform :8700 + Portal :8701), session-scoped E2E fixtures, andmake dev-e2etarget - ORM E2E Tests (billing):
test_billing_workflow.py— order-to-invoice, invoice-to-payment, proforma conversion, full/partial refund flows using Django TestCase with direct DB access; Romanian VAT rate sourced fromTaxService.get_vat_rate("RO")(no hardcoded percentage) - ORM E2E Tests (signup):
test_signup_workflow.py— complete company and individual customer signup, GDPR consent tracking, multi-address support, user registration and onboarding steps; both files bootstrap viadjango.setup()with E402 noqa on post-setup imports and are marked@pytest.mark.e2e
- E2E Portal Navigation Assertions:
verify_role_based_contentnow treatssuperuserandcustomeridentically on the portal — both check for/tickets/and/billing/links; removed the stalesuperuserbranch that asserted/app/and/customers/which are platform-only routes at :8700 - E2E Test Quality:
navigation.pycatches onlyPlaywrightTimeoutErrorin admin-blocked check (unknown exceptions now returnFalseinstead of silently passing), replaces stale/admin/expectation with/app/, and letsverify_role_based_contentfailures propagate;monitoring.pyskips HTMX extended selectors (closest/find/next) inhx-targetcheck;test_navigation.pyraises success threshold from>0to>=75%of sections - Security Scanner Severity Filter:
security_scanner.pyreplaces lexicographic severity string comparison with a numeric rank map (CRITICAL=4…INFO=0) so--min-severity HIGHcorrectly includesCRITICALfindings - E2E Signup Flow Tests: Disabled CSS monitor (
check_css=False) ontest_signup_then_login_flowandtest_complete_new_customer_journey— both tests navigate across multiple pages (signup -> login -> dashboard), destroying the original page execution context and causing the CSS monitor to raise spurious failures - E2E Test Suite: Fixed 37 test issues (19 assertion failures + 18 teardown errors) caused by stale
.pyccache and incorrect test selectors/assumptions — zero app code changes, all test bugs - E2E Cache Prevention: Added
PYTHONDONTWRITEBYTECODE=1toconftest.pyand__pycache__cleanup to allmake test-e2e*Makefile targets to prevent stale bytecode issues in Docker bind mounts
- E2E Helpers Refactor: Extracted focused helpers package (
tests/e2e/helpers/) from monolithicutils.py— navigation, monitoring, interactions, auth, and constants are now separate modules - OS-Scoped Dev Database: Platform dev database is now
db-{darwin,linux}.sqlite3to prevent SQLite corruption when macOS host and Docker container share the same bind-mounted directory (VirtioFS cannot coordinate file locks cross-platform) - E2E Rate Limit Guard:
make test-e2enow detects active rate limiting and fails fast with actionable error instead of running 179 tests that will all fail - CSS Build Portability:
make build-cssgracefully skips when npm is not available (Docker container support) - pre-commit hook patching:
scripts/patch_precommit_hook.pynow patches all pre-commit-generated hooks (not justpre-commit), uses a versionedPATCHED_MARKERsentinel for true idempotency, switches fromuname -s | trto a POSIXcasestatement for OS detection, and resolves repo root viagit rev-parse --show-toplevel
- Lint Zero-Debt: Eliminated all Ruff violations across Platform and Portal services — zero warnings, zero errors
- Portal Lint: Reduced portal lint debt with code fixes and type annotations
- Platform Lint: Reduced lint debt across billing, orders, API, audit, common, and remaining apps
- Lint Infrastructure: Fixed URL collisions, deploy check, and test suppressions in lint tooling
- SettingsService Coverage: Wired 78 hardcoded constants to
SettingsServicewith getter functions, backward-compatible aliases, andDEFAULT_SETTINGSentries (224 total keys)
- Type Stubs: Added type stubs and expanded MyPy overrides for third-party libraries
- Settings Allowlist: Added
scripts/settings_allowlist.txtfor structural constants that cannot be runtime-configurable
- pyproject.toml: Updated lint configuration — scoped Ruff rules for runtime import architecture, expanded MyPy overrides
- Audit Enforcement: Added ADR-0016 and a model allowlist with justification requirements to formalize audit-trail coverage policy
- Audit Coverage Tests: Added runtime model-classification checks and signal-registration regression tests for critical apps
- Audit Pipeline Tests: Added integration/E2E tests for settings, billing, notifications, and customer audit event creation paths
- Signal Registration: Restored
ready()signal imports forbilling,orders,customers, anddomainsapps to ensure receivers are connected at startup - Tax Rate Migration Drift: Added migrations to align
InvoiceLine.tax_rateandOrderItem.tax_rateschema metadata with current model definitions - Portal Security: Hardened security defaults and removed lint regressions
- CI Pipeline: Fixed baseline SHA fetching and hardened ruff no-new-debt baseline resolution
- Portal Isolation: Hardened runtime and E2E test settings to enforce stateless behavior
- E2E Workflow: Stabilized development and test workflow
- Pre-commit Hooks: Stabilized compatibility and enabled configured hooks with local cache isolation
- Audit Coverage: Enforced signal wiring and model audit lifecycle coverage for all critical models
- Platform Lint Debt: Reduced technical debt with safe complexity and security fixes
- Repository Normalization: Applied repository-wide normalization and refactor updates
- Settings: Corrected default values, removed stale caches, and added missing configuration keys identified during code review (#9)
- Billing: Replaced deprecated
CheckConstraint.checkwith.conditionto align with Django 5.2 API changes
- Configuration Sprawl Cleanup: Eliminated hardcoded
ROMANIAN_VAT_RATEfrom 5 locations (constants.py,types.py,context_processors.py,products/signals.py,base.py). All callsites now useTaxService.get_vat_rate('RO')per ADR-0005/ADR-0015 - Billing Terms Consolidated: Wired invoice payment terms, proforma validity, and payment grace period through
SettingsServicewith proper fallback cascade. Renamed setting keybilling.invoice_due_days→billing.invoice_payment_terms_dayswith data migration preserving admin overrides - Invoice Payment Terms Corrected: Default payment terms aligned to 14 days across
constants.py,SettingsService, andbilling/config.py(previously 30 in constants, 14 in config — now consistent) - Page Size Unified:
DEFAULT_PAGE_SIZEcorrected from 25 → 20 acrossconstants.py,mixins.py, and billing views (previously inconsistent between modules) - Proforma/Invoice Views Dynamic: 7 hardcoded
timedelta(days=30)andDecimal("21.00")VAT values inbilling/views.pyreplaced with SettingsService and TaxService calls
ROMANIAN_VAT_RATEandROMANIAN_VAT_RATE_PERCENTfromconstants.pyandtypes.py(useTaxServiceinstead)VAT_RATEandROMANIA_VAT_RATEfromconfig/settings/base.py(redundant with TaxService)- Dead constants:
PASSWORD_RESET_TOKEN_VALIDITY_HOURS,EMAIL_SEND_RATE_PER_HOUR(never imported, conflicted with authoritative sources) - Dead SettingsService key:
users.password_reset_timeout_hours(Django'sPASSWORD_RESET_TIMEOUTis authoritative) - Dead alias:
INVOICE_DUE_DATE_DAYSfrombilling/config.py(zero consumers)
get_invoice_payment_terms_days()inbilling/config.py— reads from SettingsService with env-var fallback, positive-value clamping, and logged exception handling- Data migration
0002_rename_invoice_due_days_key— idempotent rename with key-collision handling - 12 guardrail tests preventing configuration drift:
test_constants_consistency.py: VAT sprawl guard, billing term sync, page size consistency, dead constant detection,calculate_romanian_vatTaxService integration, context processor regressiontest_billing_terms.py: SettingsService billing term defaults and DB override integration tests
- ADR-0015: Configuration Resolution Order — Documents the 4-tier configuration cascade pattern (Cache → DB → Settings → Code Defaults) as a platform-wide architectural standard, with decision criteria for when to use each tier
- Temporal VAT Rate Support:
TaxRulemodel now seeds historical (19%, pre-Aug 2025) and current (21%, post-Aug 2025) Romanian rates viasetup_tax_rulesmanagement command - Per-Customer VAT Overrides: Wired orphaned
CustomerTaxProfilefields (is_vat_payer,vat_rate,reverse_charge_eligible) into the VAT calculation flow viaTaxServiceandOrderVATCalculator - VAT Guard Test: Grep-based test that scans
apps/for hardcodedDecimal("0.19")orDecimal("19.00")outside allowlisted files, preventing future rate sprawl - Temporal VAT Boundary Tests: Tests verifying correct rate resolution at the July 31 / August 1, 2025 transition boundary
- E2E Test: Playwright test verifying proforma form dropdown shows 21%/11%/0% with no stale 19%
- Single Source of Truth for VAT:
TaxServiceis now the sole VAT rate authority —billing.config.get_vat_rate()delegates toTaxServiceinstead of independently queryingTaxRule - TaxService Database Tier Fixed:
_get_rate_from_database()now queries the realTaxRulemodel instead of non-existentTaxSettings - Romanian VAT Rate Updated to 21%: All hardcoded 19% references updated across billing views, model defaults, PDF generators, e-Factura settings/validator, sample data generators, proforma templates, and documentation (per Emergency Ordinance 156/2024, effective August 1, 2025)
- Romanian Reduced VAT Rates Consolidated: 5% and 9% reduced rates merged to single 11% rate across e-Factura settings, validator, and proforma form templates
- ADR-0005 Amended: Added scope clarification distinguishing value-immutable constants from regulatory/temporal values, with forward reference to ADR-0015
- Proforma Form Value/Label Mismatch (CRITICAL):
<option value="19">21% (Standard)</option>— the submitted value was 19 while the label showed 21%. Both value and label now correctly show 21% - e-Factura XML Tax Rate:
xml_builder._get_tax_rate()now reads from the invoice's stored line tax rate (frozen at creation) instead of liveTaxService, preserving document immutability for regulatory compliance - Custom VAT Rate Guard: Fixed
Decimal("0.00")being falsy — changedif tax_profile.vat_rate and ...toif tax_profile.vat_rate is not None:to correctly apply 0% VAT overrides - TaxService
calculate_vat()Business Flags:is_businessandvat_numberparameters were accepted but completely ignored — now properly trigger reverse charge for EU B2B transactions - Non-EU Default Rate: Countries without explicit
TaxRulerecords now fail-safe to Romanian VAT (21%) instead of silently returning 0%
- Per-customer reverse charge eligibility now enforced in VAT calculation (previously orphaned field)
- Invoice tax rates frozen at document creation time, preventing retroactive rate changes on issued documents
- Production deployment and hardening
- Complete template system with polished UI
- Comprehensive test coverage (>90%)
- Production deployment guides and Docker optimization
- Advanced business intelligence dashboards
- Mobile application for technicians
- Multi-tenant architecture for resellers
- API-first architecture with GraphQL
- GDPR Cookie Consent on Portal: Moved cookie consent system from Platform (staff-only) to Portal (customer-facing) where GDPR compliance actually matters
- Cookie consent banner with granular per-category controls (essential, functional, analytics, marketing)
- Cookie policy page accessible without authentication, bilingual (RO/EN)
- Footer links for Cookie Policy and Cookie Preferences re-opening
- Server-side consent recording via HMAC-authenticated Platform API
- Anonymous visitor consent via
cookie_id, linked to user account on login
- GDPR API Namespace (
/api/gdpr/): Three new Platform endpoints for Portal-to-Platform GDPR communicationPOST /api/gdpr/cookie-consent/— Record consent (anonymous or authenticated)POST /api/gdpr/consent-history/— Fetch consent history for authenticated usersPOST /api/gdpr/data-export/— Request GDPR data export (Article 20)
- Portal GDPR Views Wired to Real Data: Consent history and data export views now call Platform API instead of using mock/TODO stubs
- Audit Coverage: Security logging for payments, notifications, tickets, and promotions via centralized AuditService
- ADR-0014: No-test-suppression policy with automated scanner (
scripts/lint_test_suppressions.py) integrated intomake lint - Audit Coverage Scanner (
scripts/audit_coverage_scan.py): Automated detection of unaudited security-sensitive operations
- E2E Test Suite Stabilized: 166/166 passing (was 76 failing), removed 11 duplicate test files (-11.4k lines)
- Portal membership cache uses TTL-based invalidation (5-min expiry) to prevent stale session data
- Portal role resolver performs fresh fetch from Platform API before fallback
- Rate limiting middleware respects
RATELIMIT_ENABLEDjango setting and environment variable - DRF throttling disabled in test and dev-test environments
- Infrastructure URL wiring in Platform router and nav context processor
- Portal login membership caching (populate
user_membershipsin session on login) - Portal ticket creation API call signature (
dicttoTicketCreateRequest) getattrinstead ofhasattrfor_portal_authenticatedcheck (defensive coding)- Hardcoded
/cookie-policy/URL replaced with{% url 'cookie_policy' %}in Portal footer - Unused
import jsonremoved from E2E test module - Flaky
page.on('response')replaced with deterministicpage.expect_response()in E2E tests
- HMAC staff session bypass restricted from all
/api/*to explicit allowlist - Portal role fallback hardened:
ownerrole for verified primary customer only - Customer create API now requires HMAC authentication
@throttle_classes([])on GDPR API views to bypass DRF global throttle on service-to-service endpoints- Cookie consent signal (
cookie_consent_updated) now emits for audit trail creation
- e-Factura Integration: Complete Romanian electronic invoicing with XML generation and ANAF submission
- Subscription Billing: Recurring billing engine with PDF invoice generation
- Usage-Based Billing: Metering and tiered pricing system for hosting resources
- Promotions System: Coupons, discounts, and loyalty program management
- Multi-Provider Email: Pluggable email sending infrastructure (SMTP, SendGrid, Mailgun)
- VPS Node Deployment: Terraform + Ansible automation for server provisioning
- AES-256-GCM Encryption: Enhanced credential and data encryption at rest
- SIEM Integration: Security event logging and compliance monitoring
- GDPR Cookie Consent: Cookie consent banner and legal pages
- File Integrity Monitoring: Upload security and integrity checking
- Dynamic Analysis Middleware: Trace-based runtime analysis tooling
- Static Flow Analysis: Codebase analysis tooling
- E2E Test: Signup-to-order flow end-to-end test
- Idempotent Rollback Tracking: Enhanced provisioning rollback reliability
- CI/CD migration from pip to uv package manager
- CI workflows upgraded setup-uv from v4 to v7
- Comprehensive test coverage infrastructure improvements
- Portal security hardening and input validation
- Race conditions in webhook and refund processing
- Pre-existing test failures in CI pipeline
- Merge conflict resolution across 14 files
- Portal requirements for Django 5.2 and python-ipware compatibility
- OWASP vulnerability remediation (P1-P3 audit findings)
- SSL/TLS configuration hardened for production
- Security audit findings addressed across platform
- Caching, connection pooling, and rate limiting middleware
- Platform/Portal Service Separation: Complete architectural split into two Django services
- Platform Service (staff/admin): Business operations, billing, provisioning, customer management
- Portal Service (customer-facing): Order placement, service management, account self-service
- HMAC API Authentication: SHA-256 signed inter-service communication
- Stripe Payment Integration: Complete payment flow with payment intents and webhook processing
- Async Provisioning: Celery-based task queue for service provisioning with failure tracking
- Order Checkout Flow: End-to-end order placement from portal through platform API
- CI workflow requirements paths updated for services architecture
- Django test runner configured for platform CI
- Services-Based Architecture Migration: Complete restructure into
services/platformandservices/portallayout - Production-ready Virtualmin optimizations and architecture improvements
- Monorepo layout established for multi-service architecture
- Refund service transaction handling improvements
- Credential vault enhancements
- Virtualmin Integration: Two-phase provisioning with rollback capability
- Pre-flight validation (server capacity, domain availability, resource limits)
- Ordered rollback operations for failed provisioning
- Credential Vault: Fernet-encrypted credential storage with monthly rotation
- PRAHO-as-Source-of-Truth: Authoritative data model driving all Virtualmin operations
- Multi-Path Authentication: ACL risk mitigation with SSH/sudo fallback
- Production Safety: Health checks, rate limiting, retry logic with exponential backoff
- Virtualmin Account Protection: Security system for provisioned accounts
- Comprehensive type safety and code quality improvements
- Migrated from Celery to Django-Q2 for task processing
- Feature-based file organization for provisioning and customers apps
- Secure IP Detection: Centralized
get_safe_client_ip()with CIDR-based trusted proxy configuration (IPv4/IPv6) - HTTPS Security Hardening: Environment-specific SSL/TLS configuration
- Production: SSL redirect, secure cookies, HSTS (1 year)
- Staging: Flexible HTTPS with shorter HSTS for rollback safety
- Development: HTTPS disabled for local development
- Email Enumeration Prevention: Uniform response system eliminating account discovery attacks
- Zero database queries, consistent timing with jitter, same HTTP status regardless of email existence
- System Settings App: Centralized configuration management with category-based organization
- 100% lint compliance achieved across entire platform
- Cross-app security hardening with comprehensive test coverage
- Billing system security enhancements
- Settings encryption and access control refinement
- IP spoofing attack prevention with CIDR-based proxy validation
- Production HSTS (1 year), secure cookies, SSL redirect
- Email enumeration vulnerability eliminated (OWASP A01, A04, A07)
- Django system checks for security configuration validation
- Complete Domain Management System
- TLD management with registration/renewal/transfer pricing
- Multi-registrar framework with cost tracking and profit margins
- Domain lifecycle: registration, renewal, transfer, expiration monitoring
- Romanian-specific TLD support (
.ro,.com.ro) - Domain-order integration with
DomainOrderItemmodel
- Service Relationships & Groups
- Parent-child service hierarchies (hosting -> domains -> SSL)
- ServiceGroup and ServiceGroupMember for package management
- ServiceDomain model for service-domain binding
- System Settings: Centralized configuration with category-based organization
- Comprehensive test suites for common, customers, orders, tickets
- Enhanced provisioning UI templates
- Staff management E2E test suites
- GDPR Compliance System: Complete Romanian GDPR (Law 190/2018) implementation
- Data export with immediate JSON download
- Secure data deletion workflow with confirmation
- Consent history tracking with timeline visualization
- Privacy dashboard for user self-service
- Comprehensive Audit System: 200+ categorized action types with signal-based logging
- Authentication audit with security logging
- Business transaction audit with 100+ event types
- GDPR management dashboard for staff
- Order Management: Complete order lifecycle with Romanian VAT compliance
- Bidirectional Refund System: Order-invoice synchronized refund processing
- Full and partial refunds with amount validation
- Refund reason categories for audit compliance
- Payment gateway integration ready (Stripe/PayPal)
- Modernized navigation header with dropdown menus
- BREAKING: Django Admin Interface completely removed (4,239 lines across 11 files)
- Replaced by custom staff interface at
/app/ - Following NetBox v4.0 pattern for hosting platforms
- Staff users must use
/app/instead of/admin/
- Replaced by custom staff interface at
- Order Management System: Complete order lifecycle with status workflow
- Status progression: draft -> pending -> processing -> completed -> refunded
- Romanian VAT-compliant order totals with sequential numbering (ORD-YYYYMMDD-XXXXXX)
- Romanian Business Types System: Centralized type system for compliance
CUIString,VATString,Money,PhoneNumber,EmailAddress,DomainName- Result pattern: Rust-inspired
Ok[T]/Err[E]error handling - Django integration types:
RequestHandler,AjaxHandler,HTMXHandler
- Type Safety Enhancement: 33.4% reduction in type errors (842 -> 561)
- 170 ANN001 + 111 ANN201 errors fixed
- MyPy strict mode configured with Django type stubs
- Strategic Linting Framework: Ruff + MyPy with business-focused rules
- Modernized UI: Shadcn-style pagination, badge component
- Comprehensive E2E testing (invoices, tickets, mobile)
- Complete Romanian translations for dashboard
- Django upgrade from 5.0 to 5.2
- Code deduplication: consolidated phone/CUI/VAT validation into centralized types
- JSON response standardization with
json_success()/json_error()
- N+1 query optimization for User model methods (smart prefetch detection)
- 10 PERF401 performance anti-patterns eliminated
- Secure Password Reset: Rate-limited (5/hour per IP) with 2-hour token expiry
- Comprehensive audit logging, bilingual templates (RO/EN)
- Account lockout reset on successful password change
- 2FA Encryption: TOTP secrets encrypted at rest using Fernet encryption
- Backup Codes System: 8 secure one-time use recovery codes per user
- Hashed with Django password hashers, automatic consumption after use
- 2FA Recovery Flow: Complete recovery for lost authenticator devices
- Session Security System: Dynamic role-based timeouts
- Admin/Billing: 30 min, Standard: 1 hr, Shared Device: 15 min, Remember Me: 7 days
- Automatic session rotation on password/2FA changes
- Multi-IP detection for suspicious activity (3+ IPs within 1 hour)
- Enterprise Security Framework: Validation decorators and rate limiting
@secure_user_registration,@secure_customer_operation,@atomic_with_retry- Injection attack prevention (XSS, SQL injection, code execution patterns)
- Privilege escalation prevention
- Customer User Assignment: Three-option workflow (create, link, skip)
- Ticket Replies System: Comprehensive reply system with internal comments
- Mobile Navigation: Responsive header with DRY components
- UI Components: PRAHO favicon, consistent checkbox component, auth page branding
- CSRF exemption removed from email check API (OWASP A04)
- XSS vulnerabilities in templates:
|safereplaced with|escape(OWASP A07) - Cryptographic security: insecure random replaced with
secretsfor MFA backup codes - Enhanced Content Security Policy headers
- OWASP A01 (Access Control), A02 (Crypto), A03 (Injection), A04 (Design), A07 (Auth)
- Rate limiting on registration (5/hr), invitations (10/hr), company validation (30/hr)
- Initial Release: Complete hosting platform foundation for Romanian providers
- 8 Django Apps: Users, Customers, Billing, Tickets, Provisioning, Audit, Common, UI
- Email-Based Authentication: Custom user model (no usernames), profile system, customer memberships
- Customer Management: Normalized profiles (Tax, Billing, Address), CUI/VAT validation, multi-user access
- Billing System: Proforma/Invoice models, sequential numbering, multi-currency (RON/EUR/USD)
- Tax/VAT Compliance: Romanian 19% VAT, EU cross-border handling, VIES integration ready
- Dunning System: Automated payment retry with configurable per-tier policies
- Product Catalog: Hosting products with multi-currency pricing and billing cycles
- Order System: Complete order lifecycle management with Romanian compliance
- Support Tickets: SLA tracking, ticket numbering (TK2024-XXXXX), file attachments, time tracking
- Service Provisioning: Plans, server management, lifecycle tracking, Virtualmin API ready
- Audit & Compliance: Immutable logging, GDPR tracking, Romanian compliance
- Notifications System: 14 bilingual email templates (RO/EN) for all customer communications
- Billing, payment reminders, service activation, support, onboarding
- Webhook Deduplication: Stripe-ready event processing with exponential backoff retry
- Domain Management: Multi-registrar support (.ro via ROTLD, international via Namecheap/GoDaddy)
- Service Relationships: Parent-child hierarchies, service groups for hosting packages
- UI Components: Template tags for Romanian business formatting, HTMX foundations
- Database Performance: Composite indexes for orders, domains, services, and provisioning tasks
- Argon2 password hashing, CSRF protection, secure cookies
- Append-only audit trails for forensic analysis
- e-Factura compliance ready (XML generation)
- GDPR data export, erasure, and consent tracking
| Version | Date | Milestone |
|---|---|---|
| 0.14.0 | 2026-02-12 | VAT Architecture Consolidation & ADR-0015 |
| 0.13.0 | 2026-02-11 | GDPR Cookie Consent, Audit Coverage & E2E Stabilization |
| 0.12.0 | 2026-02-10 | Billing, e-Factura & CI Stabilization |
| 0.11.0 | 2026-02-09 | Platform/Portal Service Separation |
| 0.10.0 | 2025-09-05 | Services Architecture Migration |
| 0.9.0 | 2025-09-04 | Virtualmin Integration |
| 0.8.0 | 2025-08-31 | Security Infrastructure & Compliance |
| 0.7.0 | 2025-08-30 | Domain Management & System Settings |
| 0.6.0 | 2025-08-28 | GDPR Compliance & Admin Removal |
| 0.5.0 | 2025-08-27 | Order Management & Type Safety |
| 0.4.0 | 2025-08-25 | Security & Authentication Hardening |
| 0.3.0 | 2025-08-20 | Initial Release - Core Foundation |
For detailed technical information, see ARCHITECTURE.md and the /docs/decisions/ folder for Architecture Decision Records.