Skip to content

feat(lang): full Polish translation, invoice qty, VAT labels, client NIP + password generator - #8

Open
hedon77 wants to merge 90 commits into
Panelica:mainfrom
hedon77:feature/polish-translation
Open

feat(lang): full Polish translation, invoice qty, VAT labels, client NIP + password generator#8
hedon77 wants to merge 90 commits into
Panelica:mainfrom
hedon77:feature/polish-translation

Conversation

@hedon77

@hedon77 hedon77 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Localization and billing/clients improvements for the PNLCS panel. PR includes full Polish translation of the admin and client areas plus several feature fixes discovered while testing the translation.

The invoicing work in particular aims to adapt the billing to our Polish legal requirements (split VAT rates, per-line VAT, PDF on the invoice/reminder emails, payment-due terms).

Changes

1. Full Polish translation

  • Translated the complete admin and client areas into Polish (lang/pl/*)
  • Added missing form.company key and fixed client name label
  • Payment method labels translated (banktransferPrzelew) via a new payment_method_label() helper; EN keeps "Bank Transfer"
  • Product autocomplete and live tax summary (Net/VAT/Gross) in the invoice form

2. Invoice builder — quantity (qty)

  • New migration adding qty column (int, default 1) to invoice_items
  • InvoiceService::addLineItem() accepts qty, recalculateTotals() multiplies amount × qty
  • Admin create form: "Ilość" column with live line total; validation items.*.qty (int, min 1)
  • Admin/client/PDF views now show Qty/Price/Total columns

3. Invoice label fixes

  • Tax label shown as VAT in Polish (pdf.tax, admin.invoices.tax, common.table.tax)
  • Trailing zeros stripped from tax rate display (e.g. VAT (23%) instead of VAT (23.00000%))
  • Currency suffix fixed on the server DB (PLN → " zł" with a leading space)

4. Custom client fields + buyer snapshot

  • Config CRUD for client custom fields (show on invoice / admin only)
  • Custom fields marked "show on invoice" are frozen into the invoice buyer snapshot (buyer_custom_fields)
  • Displayed on admin client forms and the issued invoice PDF

5. Client login accounts

  • Staff can set a login password when creating/editing a client
  • Client profile page: change password (with strength meter) + edit own custom fields (admin_only=false)

6. Client form — NIP + password generator

  • New NIP (tax_id) field on client create/edit forms and on the summary view
  • Moved password fields to a dedicated section (separated by a divider like custom fields)
  • Added a colored password strength meter and a Generate Password button

7. Client phone — country-driven prefix

  • Client create/edit forms: phone prefix select that auto-follows the chosen country
  • phone_prefix column on clients (varchar 4) with full_phone accessor on the model

8. Invoice view fixes

  • Invoice total label now reads Suma
  • Amounts in the line-items and totals columns no longer wrap, so a figure and its currency suffix stay on one line
  • Payment method displayed via payment_method_label() (Przelew instead of raw banktransfer)

9. Client — default payment method

  • New default_payment_method column on clients
  • Select on client create/edit listing usable gateways + Przelew + Gotówka
  • Payment method is now required on clients and on new invoices (no None option)
  • The client's default pre-fills the payment method on the invoice creation form

10. General setting — default payment method for new clients

  • New DefaultPaymentMethod setting in System Settings (Ustawienia ogólne)
  • Seeded on the live server so new installs default to banktransfer
  • Client create form pre-fills the default payment method from this setting

11. Client create form — password generator

  • Password section moved to a dedicated block with the Generate Password button and colored strength meter (same as the edit form)

12. Billing period on invoice items

  • Renewal invoice line items now read e.g.
    Obsługa serwera VPS (Monthly) — test.pl — 2026/08/15 - 2026/09/15
  • The period is derived from next_due_date and the billing cycle, so itemized periods match the covered interval
  • New order line items show the paid period in the parentheses instead of the raw cycle name, e.g.
    Obsługa serwera VPS (2026/08/15 - 2026/09/15) — test.pl

13. Service — editable "Next Due Date"

  • New PUT admin/services/{service}/next-due route (under manage_services permission)
  • Inline date field on the admin service page (billing panel) that lets staff change next_due_date
  • Red date border shown when the service is overdue; field disabled for terminated services

14. Configurable invoice numbering scheme

  • New InvoiceNumberFormat setting in System Settings (Ustawienia ogólne), displayed under "Default Payment Method for New Clients"
  • Format accepts {year}, {yy}, {month}, {day} and {num} placeholders (e.g. INV-{year}{month}-{num}INV-202608-000008)
  • Optional "Reset numbering each year" checkbox: only invoices issued in the current year count, so January starts a fresh sequence
  • Live preview beside the field renders the next number automatically from the format and the last invoice in the database
  • {num} is padded to 6 digits and continues the sequence across format changes (never reissues a number)
  • PDF download sanitizes the invoice number in the file name, so schemes using / or \ (e.g. FA-2026/08-000008) still download
  • The "Default Payment Method for New Clients" and "Invoice Numbering Scheme" fields are constrained to half the card width (responsive), stacked under one another

15. Service — status management buttons

  • New status card at the top of the admin service page with buttons to switch a service to Active / Suspended / Terminated / Pending / Cancelled (WHMCS color coding)
  • New PUT admin/services/{service}/status route (updateStatus) and DELETE admin/services/{service} route (destroy, under manage_services)
  • Changing status also maintains suspension_date/termination_date; the Delete button permanently removes the service (cascade cancels its addons)

16. Semi-Annually next-due date fix

  • OrderService::calculateNextDueDate() matched the cycle as semi-annually while the rest of the app (form, validation, BillingCycleHelper, Service::monthsInCycle) uses semiannually, so a Semi-Annually order got a next_due_date one month out instead of six
  • calculateNextDueDate() now delegates to BillingCycleHelper::advance(), the single source of truth for cycle lengths

17. Client domain transfer with EPP code

  • New GET client/domains/transfer page with a domain + EPP/Auth code form; the code is validated (required_if type=transfer) and stored on the cart item
  • New epp_code column on domains (migration) and the value is persisted when the order is placed
  • OrderService::registerOrderedDomain() now actually initiates a transfer through the registrar ($registrar->transfer($domain, $eppCode)) instead of leaving it "to be done by hand"
  • The domain search now offers the Transfer action for any domain that is not confirmed available (previously only when the WHOIS check was able to confirm "taken", and .pl had no WHOIS server configured)
  • "Register domain" / "Transfer domain" buttons on the client's domain list (were dead # links)

18. HRD (hrd.pl) registrar

  • New registrar module Modules\Registrars\HRD\HrdRegistrar wired to the hrd/hrd-api PHP SDK (Composer VCS repo consultingservice/hrd-api)
  • Config: api_login, api_hash, api_pass and optional default_ns_group for registrations
  • Implements register (creates an HRD registrant user then domainCreate), transfer (domainTransfer with the auth code), renew, nameserver get/set, EPP code retrieval (domainTradeGetPw) and syncDomain
  • Registered as hrd in ModuleServiceProvider; verified on the live server (SDK autoloads, module resolves with 4 config fields)

19. Admin domain view (WHMCS-style)

  • The admin domain detail page now has a management toolbar, styled like the Services view:
    • Sync — pulls expiry/status/nameservers/lock back from the registrar (syncDomain)
    • Renew — renews for a chosen number of years via DomainService::renewDomain
    • Lock/Unlock — toggles the registrar transfer lock
    • Auto-renew — toggles auto-renewal (payment_method)
    • EPP code — retrieves and shows the transfer code
  • Editable nameserver form (NS1–NS5) and a registrar select in the Registration panel
  • New admin routes: domains.{sync,renew,nameservers,lock,autorenew,epp,registrar}
  • "View" action added to the client's Domains tab in the admin client profile

20. Client domains

  • "View" action (and clickable domain name) on the client's domain list, linking to the domain detail
  • Fixed the client domain detail: EPP code now returns via redirect/flash (was JSON against a GET route) and nameservers are read from the nameservers column (was reading non-existent ns1..ns5)

21. Settings — Domains section

  • New Domains section in General Settings with five default nameservers (DefaultNameserver1..5)

22. Settings — Invoices section

  • New Invoices section grouping the default payment method, the invoice numbering scheme and its yearly-reset switch
  • New default payment due (days) setting (InvoiceDueDays, default 14)
  • The due date is honoured when creating invoices (InvoiceService, the renewal generator and the admin "new invoice" form)
  • The Send test email button now lives inside the Mail Configuration section

23. Invoice builder — per-item VAT

  • The "taxed" checkbox is replaced with a VAT % field per line item (new tax_rate column on invoice_items)
  • recalculateTotals() sums VAT per line from its own rate; legacy invoices without a per-line rate keep using the invoice-level rate
  • Columns renamed Cena (unit price) and Suma (line total); the summary breaks VAT out per rate (VAT 23%, VAT 7%, …)

24. Invoice view — inline editing

  • In the admin invoice detail, description / qty / price / VAT are editable inline (click → type → Enter saves and recalculates the totals)
  • New PUT admin/invoices/{invoice}/items/{item} route; editing a legacy line converts its siblings to explicit rates so the tax stays consistent

25. Invoice view — send / remind

  • Send button emails the invoice to the client (InvoiceCreatedMail); Remind sends a payment reminder (PaymentReminderMail, only for unpaid/overdue invoices)
  • Both the invoice email and the reminder now attach the invoice PDF
  • "Mark paid" label renamed to Opłacona ("Paid")

26. Invoice activity log

  • A scrollable Dziennik zdarzeń panel shows the last 4 events per invoice (created, line edited, sent, reminded, paid) with timestamp and who did it
  • New invoice_id column on activity_logs; events older than the 50 most recent per invoice are pruned automatically

27. Invoice view — add / remove line items

  • + Dodaj pozycję button in the line-items header adds a blank row (POST admin/invoices/{invoice}/items)
  • A red × per row deletes it after confirmation (DELETE admin/invoices/{invoice}/items/{item}), matching the create form's look

28. Invoice list — bulk actions

  • Checkboxes with a select-all toggle on /admin/invoices
  • Bulk toolbar: Opłacona (mark paid), Anuluj (cancel), Wyślij (send), Przypomnij (remind) — applied to every selected invoice (POST admin/bulk/invoice-action)

Testing

  • Verified on the live server (PNLCS@pnlcs.24box.pl): migrations, view:cache, translation cache refresh
  • Invoice totals with qty verified via tinker (3×100 + 2×50 → subtotal 400.00, tax 69.00, total 469.00)
  • Custom field VAT/NIP snapshot verified on an issued invoice
  • Client login + password change verified through the client area
  • Billing periods verified on the live server for all cycles (Monthly/Quarterly/Semi-Annually/Annually/Biennially/Triennially)
  • Order line item description verified (billing period shown instead of the cycle) on the live server
  • Next due date update verified on the live server; route:list confirms the new route is registered
  • Invoice numbering scheme verified on the live server: last invoice INV-000007 → next INV-202608-000008 under the seeded INV-{year}{month}-{num} format
  • PDF download verified on the live server for an invoice numbered FA-2026/08-000008 → file invoice-FA-2026-08-000008.pdf (HTTP 200)
  • Service status buttons + delete verified on the live server (new routes registered via route:list)
  • Semi-Annually next-due date fix verified: calculateNextDueDate() now advances six months via BillingCycleHelper::advance()
  • Admin domain actions (sync/renew/nameservers/lock/autorenew/epp/registrar) verified via route:list on the live server
  • HRD registrar verified: SDK autoloads, module resolves, hrd registered
  • InvoiceDueDays setting verified: route:list + admin "new invoice" form defaults from the setting
  • Per-item VAT verified: tax_rate migration applied, totals recalculated from per-line rates
  • Inline editing + send/remind routes verified via route:list (invoices.items.update, invoices.send, invoices.remind)
  • Invoice activity log verified: invoice_id migration applied, events pruned beyond 50 per invoice
  • Add/remove line items + bulk invoice actions verified via route:list (invoices.items.store, invoices.items.destroy, bulk.invoice-action)

hedon77 added 12 commits August 14, 2026 20:53
… display)

- ConfigController: customFields/store/update/destroy + validation
- routes: admin/config/custom-fields* under manage_settings
- view: admin/config/custom-fields.blade.php CRUD with add/edit modals
- ClientController create/edit/show pass custom fields and persist values
- CustomField model: clientFields(), valueFor(), options()
- client create/edit forms render field by type; show displays values
- nav + translations EN/PL (admin.custom_fields.*, nav, clients.custom_fields)
- messages.success.custom_field_added/updated/deleted EN/PL
…shot

- new invoices column buyer_custom_fields (json)
- buyerSnapshotFrom copies fields flagged show_on_invoice (via
  CustomField::invoiceSnapshot) so later client edits don't rewrite
  already-issued invoices (issue Panelica#7)
- Invoice::buyerCustomFields() falls back to live values for invoices
  issued before the column existed
- shown in admin invoice view and PDF (frozen at issue time)
- admin client create form gains Password + Confirm Password (optional)
- store() creates the linked User (owner) with the hashed password, so a
  client added by staff can sign in to the client area and be impersonated
- email validated unique against both clients and users
- new keys common.form.password / form.password_confirm, admin.clients.password_optional (EN/PL)
- admin client edit form gains New Password + Confirm Password (blank leaves
  it unchanged)
- update() calls setClientPassword() which updates the owner user or creates
  one if the client has no linked login yet
- new key admin.clients.password_optional_edit (EN/PL)
@hedon77 hedon77 changed the title feat(lang): full Polish translation feat(lang): full Polish translation, invoice qty, VAT labels, client NIP + password generator Aug 15, 2026
hedon77 added 17 commits August 15, 2026 10:46
- Route gateway labels through payment_method_label() helper in client
  invoice/checkout, admin gateways config, and gateway logs
- Translate BankTransferModule payment form (details, bank fields,
  reference, amount, notes) via messages.banktransfer.* keys
- Translate raw payment_method/gateway values in order emails and admin
  order/domain/service/transaction/affiliate views
- Add banktransfer.* keys to pl and en messages.php
Lang::has('payment_method.x') looks up the 'payment_method' file, but
the keys live in messages.php, so the helper always fell back to
ucwords() and showed 'Banktransfer' instead of 'Przelew'.
@Panelica

Copy link
Copy Markdown
Owner

Thank you for this — 4,550 keys across 13 files is a serious piece of work, and the quality checks came back strong. We reviewed it the same way we review our own changes, so here is exactly what we ran and what we found.

What passed

Check Result
PHP syntax 13/13 clean
Placeholder integrity 3,710 keys compared against lang/en, 0 mismatches — every :name, :amount, :num preserved
Key parity 12 files exact, admin.php 4,549/4,550
Extra/unknown keys none
UTF-8 BOM none
Anything executable in the language files none

The placeholder result is the one we care about most — a translation that silently drops :amount from an invoice string is worse than no translation, and yours is clean across the board.

One blocker

lang/pl/admin.php starts with a stray ? before the opening tag. First bytes:

3f 3c 3f 70 68 70   =   ?<?php

The other 12 files are correct (3c 3f 70 68 70). PHP prints that character as output, so it lands in the response before any headers — we saw it live: including the file in our check script put a ? at the top of the output. Removing the single leading byte fixes it.

One small gap

admin.php is missing products.package ("Plan on the server" in English) — 1 key out of 4,550, so Polish users would see that one string in English.

Fix those two and we'll merge — everything else is ready as-is.

One thing worth saying up front, so it isn't a surprise later: PNLCS is under active development and new features keep adding new keys — this week alone we shipped buyer snapshots on invoices and the first phase of Docker app hosting, both of which introduced strings. A translation is therefore not a one-off: lang/pl will drift behind lang/en as features land. We'd rather tell you that now than have you discover it in three months. If you want to keep Polish current we're happy to flag new keys as they appear; if you'd prefer to hand it off after this PR, that is completely fine too — the work still stands on its own.

Thanks again for taking this on. 🇵🇱

@hedon77

hedon77 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the thorough review — the placeholder check and the byte-level look are exactly the kind of rigor that catches real problems. Both issues are fixed:

  • Removed the stray leading ? byte in lang/pl/admin.php (now starts cleanly with <?php).
  • Added the missing products.package key (Plan na serwerze).

On the translation drift: I'd welcome being flagged as new keys land so I can keep lang/pl current. Thanks again for the detailed feedback! 🇵🇱

@Panelica

Copy link
Copy Markdown
Owner

Both fixes confirmed, and the re-check came back clean. Thank you for turning these around so quickly.

Verified byte by byte

Check Result
First bytes of all 17 language files 3c 3f 70 68 70 — clean <?php, no BOM, no stray byte
PHP syntax 17/17
UTF-8 validity all valid
Zero-width / bidirectional characters (U+200B–200F, U+202A–202E, U+2066–2069, BOM) none
Key parity across 13 files 3,850 keys — 0 missing, 0 extra
Placeholder integrity 0 mismatches — every :amount, :name, :count preserved
Executable content in language files none
Migrations (8) all additive, all reversible, no data loss
Money maths (qty × amount, per-line tax) correct, rounded, with a fallback for pre-tax_rate rows

The products.package addition and the leading-byte fix are both in place. recalculateTotals handling legacy rows by backfilling tax_rate from the invoice rate is a nice touch — that is exactly the kind of thing that breaks quietly if it is skipped.


One blocker: three write routes are behind a read permission

In routes/admin.php, the new line-item routes sit inside the view_invoices group:

view_invoices    PUT     invoices/{invoice}/items/{item}
view_invoices    POST    invoices/{invoice}/items
view_invoices    DELETE  invoices/{invoice}/items/{item}

view_invoices is meant to be read-only, so as it stands a staff member granted only "view invoices" can add a line, change an amount, or delete one — and recalculateTotals() then rewrites the invoice total. Someone given read access to billing can turn a 1,000 invoice into a 1 invoice.

You clearly know the distinction, because send and remind in the same PR are correctly under manage_invoices — these three just landed in the wrong block. Moving them into the manage_invoices group is the whole fix.


One note, not a blocker: epp_code is stored in plain text

2026_08_15_200000_add_epp_code_to_domains.php adds epp_code as a plain string. An EPP code is enough to start a transfer away from us, so anyone who can read the domains table can move a customer's domain. Worth treating like a credential rather than an attribute — encrypted at rest, and ideally shown once rather than kept.

Happy to take the PR as soon as the permission group is moved; the note above can be a follow-up if you would rather keep this one focused.


On keeping lang/pl current — yes, and here is the first batch. Since your last push we added an apps/Docker area, and these groups gained keys:

  • client.hosting.containers.* — the customer-facing app catalogue (install, states, domain linking, plan limits)
  • client.hosting.tools.* — tool shortcuts on service rows
  • client.store.res_*, client.store.included_resources — plan resources on store cards
  • client.cart.choose_app, client.cart.app_* — picking an app while ordering
  • sections.apps.*, sections.nav.docker_hosting — the public homepage showcase
  • admin.docker_apps.* — the operator screen for the catalogue

English, Chinese and Turkish are in; Polish is not, so those strings currently fall back to English. No rush at all — flagging it because you asked to be told.

hedon77 added 21 commits August 17, 2026 23:20
@Panelica

Copy link
Copy Markdown
Owner

Thank you — genuinely. Ninety commits over five days, a complete Polish locale, and a set of billing features we had left half-built is a serious amount of work, and it is work that every hosting company running PNLCS will benefit from.

You are credited permanently. Your name and profile now sit in the Contributors section of the project's site at pnlcs.com — not as a changelog line that scrolls away, but as a fixed part of the page.


Both points from the last review are resolved — verified

  • Invoice line-item routes. invoices/{invoice}/items (store / update / destroy) now sit in the manage_invoices group rather than view_invoices. Checked against fe1eadf — a read-only role can no longer change what an invoice charges.
  • EPP code at rest. 2026_08_18_000002_encrypt_epp_code_on_domains.php widens the column, encrypts existing values in place and skips rows that are already encrypted, so re-running is safe. That is exactly the right shape for this migration.

What this branch gives the project

We checked each of these against main before writing:

  • Custom fields. We shipped CustomField and CustomFieldValue models and then never wired them to anything — no route, no screen, referenced nowhere. Your admin screen, controller actions, MapsClientFields support and the accompanying guide finish a feature we had left as dead code.
  • Invoice line items with quantity and per-item tax rate. Ours carried only an amount and a boolean taxed flag. This is real WHMCS parity.
  • Domain transfers. epp_code on the domain, a client-facing transfer page and cart support — none of which existed.
  • Domain sync with last_sync, due dates set fourteen days ahead of expiry, and a registrar filter that lists only active registrars.
  • Client phone prefix and default payment method, buyer fields on invoices, and activity log entries linked to the invoice they belong to.
  • Registrar and gateway administration that folds, marks what is active and can test a connection before you trust it.

One item we cannot merge as it stands: the hrd/hrd-api dependency

composer.json gains a VCS repository and an unversioned requirement:

"repositories": [{ "type": "vcs", "url": "https://gitlab.com/consultingservice/hrd-api.git" }],
"require": { "hrd/hrd-api": "dev-master" }

What that means for the people who run this software:

  1. dev-master is a moving target. It resolves to whatever sits at the tip of that branch at install time. There is no tag, no version constraint and no integrity pin, so the code changes under our users without any of us deciding that it should.
  2. The repository is not ours and is not maintained. consultingservice/hrd-api is public, created in 2018, and its last activity was 17 October 2023.
  3. composer.lock is not updated in this branch, so the manifest and the lock file disagree. composer install will not fetch the package — and HrdRegistrar will fatal with a missing HRDBase\Api\HRDApi — while composer update will pull the moving branch.
  4. The module delegates all network and protocol work to that package. HrdRegistrar.php itself makes no HTTP calls, which means the part that actually talks to a registrar is the part nobody has reviewed.

PNLCS runs on other people's production servers and handles their customers' billing data. We cannot ask them to execute third-party code from an unversioned branch as a side effect of composer update.

Two ways forward, either is fine by us:

  • Preferred: implement the HRD protocol inside the module, the way Namecheap, ResellerClub and Enom already do. No external dependency, and the code is reviewable in the pull request itself.
  • Acceptable: pin hrd/hrd-api to a tagged release rather than dev-master, commit the matching composer.lock, and we will read that package before merging.

A request, not a blocker

Ninety commits and no tests. Most of this we can review by reading, but the invoice maths — quantity times amount times tax rate — decides what a customer is charged. A few cases around that would let us merge it with confidence.

Suggestion: split the branch

The translation and the billing, domain and custom-field work are ready to be reviewed and merged on their own. Keeping the HRD registrar in a separate pull request means the rest does not have to wait on the dependency question.


We are building an open alternative to a product that has had the market to itself for a long time, and contributions like this one are the only reason that is possible at all. Take the time you need — tell us when you consider the branch finished and we will review it properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants