Technical Documentation ΒΆ
This document describes the backend architecture of Gestion, a Nextcloud application for billing and invoice management. It covers the directory layout, the database schema, all service classes and their methods, routing, controllers, and external dependencies.
1. Architecture Overview ΒΆ
Gestion follows the standard Nextcloud MVC architecture. PHP controllers handle HTTP routing, services encapsulate all business logic, and a single database class (Bdd) centralises all SQL access. The frontend is a Vue/JS SPA compiled into the js/ directory.
Directory structure
| Path | Role |
|---|---|
lib/Controller/ | HTTP layer β maps routes to service calls. No business logic lives here. |
lib/Service/ | Business logic layer β each service handles one functional domain (PDF generation, company management, data access, etc.). |
lib/Db/Bdd.php | Sole database abstraction. All SQL queries are written here using whitelist-protected prepared statements. |
lib/Migration/ | Database schema migrations β one file per version bump, run by Nextcloud automatically on update. |
lib/AppInfo/Application.php | App bootstrap β registers services in the dependency injection container. |
lib/Settings/ & lib/Sections/ | Nextcloud admin panel integration β registers the Gestion settings page. |
templates/ | PHP/HTML templates rendered server-side for page views. |
src/ | Frontend source (Vue / Less). Compiled output goes into js/ and dist/. |
appinfo/routes.php | Declares all HTTP routes and maps them to controller actions. |
Database tables
All tables are prefixed with oc_gestion_. They are scoped per company via an id_configuration foreign key, allowing one Nextcloud instance to host multiple independent companies.
| Table | Content |
|---|---|
configuration | Company settings: name, address, VAT number, IBAN, invoice prefix, logo paths, shared access list. |
conf_share | Many-to-many: which Nextcloud users share access to a given configuration. |
client | Customer records linked to a configuration. |
devis | Quotes, linked to a client and a configuration. |
produit | Product / service catalogue, scoped per configuration. |
produit_devis | Join table between quotes and products (quantity, unit price override, display order). |
facture | Invoices, each linked to exactly one quote. |
Request flow
# Full request path
Browser / JS frontend
β appinfo/routes.php (URL matching)
β Controller (thin wrapper β extracts params, returns response)
β Service (business logic β validation, data assembly)
β Bdd (SQL β prepared statements)
β Nextcloud IDBConnection
Note
Controllers return either a DataResponse (JSON) or a TemplateResponse (HTML). The Bdd class always returns JSON-encoded strings from SELECT queries; services decode them as needed.
2. Routing (appinfo/routes.php) ΒΆ
Routes are declared as an associative array consumed by the Nextcloud framework. Each entry maps a verb + URL to a Controller#action pair.
| Verb | URL | Controller | Action |
|---|---|---|---|
| GET | / | ViewController | Renders the main index page |
| GET | /devis | ViewController | Renders the quotes list page |
| GET | /facture | ViewController | Renders the invoices list page |
| GET | /produit | ViewController | Renders the products page |
| GET | /config | ConfigurationController | Renders the company config page |
| GET | /isconfig | ConfigurationController | Returns whether the config is set up |
| GET | /statistique | ViewController | Renders the statistics page |
| GET | /backup | AdminController | Exports a full CSV backup to cloud storage |
| PROPFIND | /getClients | ClientController | Returns all clients as JSON |
| PROPFIND | /getDevis | DevisController | Returns all quotes as JSON |
| PROPFIND | /getFactures | FactureController | Returns all invoices as JSON |
| PROPFIND | /getProduits | ProduitController | Returns all products as JSON |
| PROPFIND | /getConfiguration | ConfigurationController | Returns the company configuration |
| PROPFIND | /getStats | StatsController | Returns entity counts |
| PROPFIND | /getAnnualTurnoverPerMonthNoVat | StatsController | Monthly turnover data (excl. VAT) |
| POST | /client/insert | ClientController | Creates a new client with placeholder values |
| POST | /devis/insert | DevisController | Creates a new quote |
| POST | /facture/insert | FactureController | Creates a new invoice |
| POST | /produit/insert | ProduitController | Creates a new product |
| POST | /update | CrudController | Generic single-field update |
| POST | /drop | CrudController | Reorders a product line in a quote (up/down) |
| POST | /sendPDF | PdfController | Emails a PDF to recipients |
| POST | /generatePDF | PdfController | Generates and saves a PDF to cloud storage |
| POST | /generateFacturX | PdfController | Generates a Factur-X PDF+XML (EN16931) |
| POST | /generateFacturXml | PdfController | Generates only the Factur-X XML file |
| POST | /sendFacturXToIopole | PdfController | Submits a Factur-X PDF to the Iopole platform |
| PUT | /duplicate | CrudController | Deep-duplicates a record |
| PUT | /createCompany | CompanyController | Creates a new company |
| DELETE | /delete | CrudController | Deletes a record |
| DELETE | /deleteCompany | CompanyController | Deletes current company and all its data |
3. Services (lib/Service/) ΒΆ
Services are the core of the application. Controllers are intentionally thin: they receive the HTTP request, call a single service method, and return its result. All validation, data assembly, and computation happen in services.
3.1 DataService
Central data-access facade. Wraps every Bdd read/write call and injects the current company ID (from the session) automatically. Controllers and other services that need data should call DataService, never Bdd directly.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
getClients() | Returns all clients belonging to the active company. | β | JSON string |
getClient($id) | Returns a single client by primary key. | $id: int | JSON string |
getClientbyiddevis($id) | Returns the client attached to a given quote. | $id: int (quote id) | JSON string |
getConfiguration() | Returns the company configuration row. | β | JSON string |
getDevis() | Returns all quotes with joined client name and company. | β | JSON string |
getFactures() | Returns all invoices with joined quote & client info. | β | JSON string |
getOneDevis($id) | Returns a single quote with full client join. | $id: int | JSON string |
getOneFacture($id) | Returns a single invoice with full join chain. | $id: int | JSON string |
getProduits() | Returns all catalogue products for the company. | β | JSON string |
getProduitsById($devisId) | Returns product lines for a specific quote. | $devisId: int | JSON string |
insertClient() | Inserts a new client with placeholder values. | β | bool |
insertDevis() | Inserts a new quote with placeholder values. | β | bool |
insertFacture() | Inserts a new invoice; auto-computes the number from the configured prefix. | β | int (new id) |
insertProduit() | Inserts a new product; reads the default VAT rate from config. | β | bool |
insertProduitDevis($id) | Attaches the first available product to a quote line. | $id: int (quote id) | int |
update($table,$col,$data,$id) | Updates a single field. Delegates to Bdd whitelist check. | table, column, value, id | bool |
updateConfiguration(...) | Updates a configuration field (no company scope check). | table, column, value, id | bool |
duplicate($table,$id) | Deep-duplicates a record; also clones product lines for quotes. | table, id | DataResponse |
drop($id,$value) | Moves a product line up or down in a quote. | id, 'up'|'down' | void |
delete($table,$id) | Deletes a record after whitelist validation. | table, id | bool |
getStats() | Returns a JSON object with entity counts: clients, quotes, invoices, products. | β | JSON string |
getAnnualTurnoverPerMonthNoVat() | Returns monthly turnover grouped by year/month (excl. VAT). | β | array |
getServerFromMail() | Reads Nextcloud system config to return the configured sender email address. | β | DataResponse |
3.2 CompanyService
Manages multi-company support and shared access. A Nextcloud user can own several companies and also be invited to access companies owned by others. The active company ID is stored in the PHP session.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
getCurrentCompany() | Returns the company ID stored in the current session. | β | int|null |
setCurrentCompany($id) | Writes the chosen company ID to the session. | $id: int | void |
getCompaniesList() | Returns all companies the current user owns or has shared access to. Auto-selects the first one if the session is empty. | β | array |
createCompany() | Creates a new company record for the current user with default placeholder values. | β | void |
deleteCompany() | Deletes the active company and all its associated data (cascade). Only the owner can delete. | β | DataResponse |
isConfig() | Checks whether the company configuration is considered complete (uses an internal changelog counter to show the setup prompt once). | β | bool |
ensureConfigExists() | Verifies that a config record exists; clears the session company if not. | β | void |
getShareUsers() | Returns Nextcloud user objects for all users who have shared access to the active company. | β | array |
addShareUser($email) | Grants shared access by Nextcloud email. Only the owner can call this. Searches all users for the matching email. | $email: string | DataResponse |
delShareUser($uid) | Revokes shared access for a given Nextcloud UID. Owner-only. | $uid: string | DataResponse |
3.3 PdfService
Handles all PDF generation and delivery. Uses the mPDF library for HTML-to-PDF rendering and GestionFacturXWriter for Factur-X metadata embedding. Also manages email delivery (legacy path) and delegates electronic invoice submission to IopoleService.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
generatePDF($html,$name,$folder) | Renders HTML as an A4 PDF via mPDF, saves it to the user's Nextcloud folder, and returns the binary as a download response. | html, name, folder | DataDownloadResponse |
generateFacturX($html,$name,$folder,$id) | Renders the PDF, builds the EN16931 XML via buildFacturXml(), embeds the XML into the PDF as XMP metadata using GestionFacturXWriter, then saves and returns the result. | html, name, folder, factureId | DataDownloadResponse |
generateFacturXml($id,$name,$folder) | Builds only the EN16931 XML (no PDF rendering) and saves it as a .xml file. | factureId, name, folder | DataDownloadResponse |
sendFacturXToIopole($html,$name,$id) | Builds the Factur-X PDF and sends it to the Iopole platform via IopoleService. Returns the Iopole invoice ID on success. | html, name, factureId | DataResponse |
sendPDF($content,$name,$subject,$body,$to,$cc) | Decodes a base64 PDF and sends it by email using Nextcloud IMailer. Returns 200 or 500 with a hint about SMTP config. | content (base64), name, subject, body, to (JSON), cc (JSON) | DataResponse |
savePDF($content,$folder,$name) | Delegates base64-encoded PDF saving to FileService. | content (base64), folder, name | void |
Private helpers
renderPdf($html)β instantiates mPDF with A4 margins, injectsstyle.css, returns raw PDF bytes.buildFacturXml($factureId)β fetches invoice + config + product lines, computes VAT totals per rate, assembles a complete EN16931-compliant XML document.buildFacturXPdfContent($html,$id)β combinesrenderPdf+buildFacturXml+GestionFacturXWriterinto a single binary string.formatAmount($amount)β rounds to 2 decimal places with a dot separator for XML output.
3.4 FacturXService
Provides a more abstract, object-oriented path to Factur-X generation. Exposes higher-level methods intended for reuse or external callers. The production path for invoices is currently handled by PdfService.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
buildXml($invoice,$company,$customer,$products) | Builds a complete EN16931 CrossIndustryInvoice XML from structured objects. Calls calculateTotals(), buildLineItems(), buildTaxes(), then assembles the full document. | invoice, company, customer, products (objects) | string (XML) |
generateFacturXPdf($pdfContent,$xmlContent) | Embeds an existing XML string into an existing PDF binary using GestionFacturXWriter. Returns the merged PDF bytes. | pdfContent, xmlContent: string | string (PDF bytes) |
Private helpers
calculateTotals($products)β iterates product lines, groups VAT amounts by rate, returnstotalHT / totalVAT / totalTTCand avatLinesbreakdown.buildLineItems($products)β generates one<ram:IncludedSupplyChainTradeLineItem>XML block per product.buildTaxes($vatLines)β generates one<ram:ApplicableTradeTax>block per distinct VAT rate.buildVatNumber($vatNumber)β ensures a French VAT number starts withFR; prependsFR00+ SIREN digits otherwise.buildDocument(...)β assembles all sections into the finalrsm:CrossIndustryInvoiceXML envelope.
3.5 IopoleService
Handles authentication and PDF upload to the Iopole electronic invoicing platform (French e-invoicing intermediary). All connection settings are read from Nextcloud app config β nothing is hard-coded.
Required app config keys
iopole_client_id, iopole_client_secret, iopole_customer_id, iopole_base_url, iopole_auth_url β all must be set in the Nextcloud admin panel before use.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
sendInvoice($pdfContent,$filename) | Authenticates via OAuth2 client_credentials, writes the PDF to a temp file, POSTs it as multipart/form-data to /v1/invoice, validates the response, and returns the parsed payload including the Iopole invoice ID. | pdfContent: string, filename: string | array (payload) |
Private helpers
requestAccessToken($authUrl,$clientId,$clientSecret)β performs the OAuth2 token exchange and returns the bearer token string.request($url,$headers,$body)β thin cURL wrapper; returns['status' => int, 'body' => string].getRequiredAppValue($key)β reads a key from Nextcloud app config and throwsRuntimeExceptionif blank.assertValidUrl($url,$key)β validates URL format withfilter_varand throws if invalid.
3.6 MailService
Dedicated service for sending a PDF as an email attachment. Uses the Nextcloud IMailer abstraction so the underlying SMTP transport is configured at the Nextcloud system level.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
sendPdf($content,$name,$subject,$body,$to,$cc) | Decodes a base64 PDF, constructs a Nextcloud mail message with HTML body and PDF attachment, handles optional CC recipients from a JSON-encoded array, and sends. Returns a status array. | content (base64), name, subject, body (HTML), to (JSON array), cc (JSON array) | array ['status' => ...] |
3.7 FileService
Low-level file access service for the current user's Nextcloud storage. Used by PdfService and TemplateService to read logos and save generated documents.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
getLogo($name) | Reads a file from the hidden .gestion/ folder and returns it as base64. Returns the string 'nothing' if the file does not exist. | $name: string | string (base64 or 'nothing') |
savePDF($content,$folder,$name) | Decodes a base64 string and saves the binary to the given Nextcloud path via saveContent(). | content (base64), folder, name | void |
saveContent($content,$folder,$name) | Creates the target folder (silently ignores "already exists"), creates the file node, and writes the raw content. | content (raw), folder, name | void |
3.8 StorageService
A more complete alternative to FileService with explicit error handling, existence checks, and deletion support. Provides a clean interface over the Nextcloud IRootFolder API.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
getStorage() | Returns the Nextcloud Folder node for the current user, or null if unavailable. | β | ?Folder |
ensureFolder($folder) | Creates a folder at the given path; silently ignores if it already exists. | $folder: string | void |
saveFile($folder,$filename,$content) | Creates the folder, creates or updates the file node, writes raw content. Returns true on success. | folder, filename, content (raw) | bool |
savePdf($base64,$folder,$filename) | Decodes base64 then delegates to saveFile(). | base64 content, folder, filename | bool |
getFile($path) | Returns raw file content for the given path, or null on error. | $path: string | ?string |
getLogo($filename) | Reads from .gestion/ and returns base64-encoded content, or 'nothing'. | $filename: string | string |
exists($path) | Checks whether a node exists at the given path. | $path: string | bool |
delete($path) | Retrieves and deletes a file node. Returns true on success. | $path: string | bool |
3.9 TemplateService
Assembles the data required for each server-rendered page and returns a Nextcloud TemplateResponse. Centralises shared base parameters (user, navigation URLs, company list) so controllers don't duplicate that logic.
| Method | Purpose | Parameters | Returns |
|---|---|---|---|
page($template) | Returns a TemplateResponse for any simple page (index, produit, statistique, etc.) with base parameters. | $template: string | TemplateResponse |
legalnotice($page) | Returns the legal notice template with the specific sub-page identifier. | $page: string | TemplateResponse |
config() | Returns the configuration template after calling ensureConfigExists(). Injects the list of shared users and attaches a permissive ContentSecurityPolicy. | β | TemplateResponse |
devisshow($id) | Fetches the quote, its product lines, and all three logo variants (main, header, footer) as base64, then renders the devisshow template. | $id: int (quote id) | TemplateResponse |
factureshow($id) | Fetches the invoice data and all three logo variants, then renders the factureshow template. | $id: int (invoice id) | TemplateResponse |
Note
The private baseParams() method builds the common data payload: user path, navigation URLs, companies list, and current company ID. It is injected into every TemplateResponse.
3.10 NavigationService
Single-purpose service that generates absolute URLs for all navigation links using the Nextcloud IURLGenerator. Used by TemplateService to inject URLs into every page.
| Method | Purpose | Returns |
|---|---|---|
getNavigationLink() | Returns an associative array of named absolute URLs for: index, devis, facture, produit, config, isConfig, statistique, legalnotice, france. | array |
4. Database Layer (lib/Db/Bdd.php) ΒΆ
Bdd is the sole class that issues SQL. It uses Nextcloud's IDBConnection (PDO-based) and protects against SQL injection through two mechanisms: prepared statements with positional placeholders for all values, and whitelist arrays for table and column names.
Security model
$whiteTable:[client, devis, produit_devis, facture, produit, configuration]β only these table names are accepted in dynamic queries.$whiteColumn: a fixed list of ~40 column names β only these are accepted asSETtargets in UPDATE calls.- All user-supplied values are passed as prepared statement parameters, never interpolated into SQL strings.
- Data is sanitised with
strip_tags()(retaining<br>) andhtml_entity_decode()before writes.
Key query methods
getDevis() / getFactures()β JOINs devis/facture with client to return a denormalized list in one query.getListProduit($devisId)β JOINs produit, produit_devis, and devis; orders by theproduit_devis.ordercolumn.gestion_update($table,$column,$data,$id,$config)β generic single-field updater after whitelist validation.gestion_duplicate($table,$id)β dynamic column-list INSERT by SELECTing the source row; handles invoice number recomputation and product line cloning for quotes.gestion_drop($id,$value)β swaps theordervalues of two adjacent product lines to move one up or down.deleteCompany($id)β cascades deletion across all 7 tables.backup()β dumps all six tables as a flat array for CSV export.getAnnualTurnoverPerMonthNoVat()β aggregates (price Γ quantity) grouped byYEAR + MONTHofdate_paiement.
Internal execution helpers
execSQL($sql,$conditions)β prepares, executes, fetches all rows, returns JSON-encoded string.execSQLNoData($sql,$conditions)β prepares and executes; no return value (INSERT/UPDATE/DELETE).execSQLNoJsonReturn($sql,$conditions)β prepares, executes, returns raw PHP array (used internally to avoid redundant JSON re-encoding).
5. Database Migrations (lib/Migration/) ΒΆ
Each migration file implements the Nextcloud SimpleMigrationStep interface and runs automatically on app update. Files are version-stamped by filename.
| Migration | Change |
|---|---|
Version7 (2022-01-25) | Initial schema: creates configuration, client, devis, produit, produit_devis, facture tables. |
Version20002 (2022-02-01) | Adds order column to produit_devis; adds path, tva_default, mentions_default to configuration. |
Version20006 (2022-02-23) | Adds devise, auto_invoice_number to configuration. |
Version20007 (2022-03-11) | Adds facture_prefixe column to configuration. |
Version20104 (2022-07-28) | Adds user_id column to devis and facture (for sequential numbering per company). |
Version20203 (2023-01-15) | Adds status_paiement to facture; adds comment to devis and facture. |
Version20204 (2023-02-02) | Adds conf_share table (shared company access); adds changelog to configuration. |
Version20300 (2023-07-14) | Adds vat column to produit; adds header bool to produit_devis (section headers in quotes). |
Version20600 (2025-03-26) | Adds vat_number, zip_code, city_name, country_code, iban to configuration. |
Version20003/04/05 (2026) | Adds zip_code, city_name, country_code to client; aligns schema for Factur-X buyer address fields. |
6. Controllers (lib/Controller/) ΒΆ
Controllers are thin by design. Each one receives injected services via the DI container and delegates immediately. The only logic present is extracting parameters from the HTTP request.
| Controller | Primary Service(s) | Responsibility |
|---|---|---|
ViewController | TemplateService | Renders all HTML page views (index, devis, facture, produit, statistique, legalnotice). |
ClientController | DataService | CRUD for clients: getClients, getClient, getClientbyiddevis, insertClient. |
DevisController | DataService + TemplateService | Reads quotes list, renders devisshow, inserts quote or product line. |
FactureController | DataService + TemplateService | Reads invoices list, renders factureshow, inserts invoice. |
ProduitController | DataService | Reads products list, reads by quote ID, inserts product. |
ConfigurationController | TemplateService + DataService + CompanyService | Renders config page, checks config state, reads and updates configuration fields. |
CrudController | DataService | Generic operations: update, duplicate, drop (reorder), delete β works across multiple entity types. |
CompanyController | CompanyService | Multi-company lifecycle: create, delete, switch active company, manage shared users. |
PdfController | PdfService | PDF generation and delivery: generate, save, send by email, generate Factur-X, submit to Iopole. |
StatsController | DataService | Returns entity counts and monthly turnover data for the statistics dashboard. |
AdminController | Bdd (direct) | Single backup endpoint β dumps all data as a dated text file in the user's cloud storage. |
7. External Dependencies ΒΆ
| Package | Type | Used for |
|---|---|---|
mpdf/mpdf | Composer (PHP) | HTML-to-PDF rendering for quotes and invoices (A4, UTF-8, custom CSS). |
GestionFacturXWriter | Bundled (Controller/) | Embeds Factur-X XML metadata into a PDF file (XMP / PDF/A-3 conformance). |
Nextcloud IMailer | Framework | SMTP abstraction for sending PDFs by email. |
Nextcloud IDBConnection | Framework | PDO-based database access; supports SQLite, MySQL, PostgreSQL. |
Nextcloud IRootFolder | Framework | User file storage access (read logos, save PDFs). |
Nextcloud IConfig | Framework | Read system and app configuration values (SMTP, Iopole credentials). |
Nextcloud ISession | Framework | Stores the active company ID between requests. |
Nextcloud IUserManager | Framework | Resolves Nextcloud user objects by email for share management. |
Tip
Composer dependencies are locked in composer.lock. Run make appstore to build a production-ready release archive.