Add containerization and improve economic invoice handling

Introduce Docker support with a Dockerfile and example docker-compose setup, enlisting services like MySQL and Redis. Enhance economic invoice drafts with support for dimensions while restructuring related logic and schemas for better maintenance and clarity.
This commit is contained in:
Jepp9350
2025-01-28 11:50:12 +01:00
parent f0747d1966
commit 0987140e61
7 changed files with 597 additions and 17 deletions
+44
View File
@@ -0,0 +1,44 @@
# Base image: PHP 8.2 with Apache
FROM php:8.2-apache
# Install necessary system dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libonig-dev \
libpq-dev \
libzip-dev \
libcurl4-openssl-dev \
default-mysql-client \
redis-server \
&& docker-php-ext-install \
mysqli \
pdo \
pdo_mysql \
zip \
bcmath \
sockets
# Enable Apache mod_rewrite (required for web server routing)
RUN a2enmod rewrite
# Set working directory
WORKDIR /var/www/html
# Copy application code
COPY . /var/www/html
# Install Composer
COPY --from=composer:2.6 /usr/bin/composer /usr/bin/composer
# Install PHP dependencies through Composer
RUN composer install --no-dev --optimize-autoloader
# Set permissions for the web server
RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html
# Expose port 80 for HTTP traffic
EXPOSE 80
# Start Redis service when the container starts
CMD service redis-server start && apache2-foreground
+42 -1
View File
@@ -34,4 +34,45 @@ $REDIS_CONFIG = [
'host' => '', // Redis host (IP address)
'database' => 0, // Redis database number (0-15)
'password' => '' // Redis password
];
];
// Set the timezone
date_default_timezone_set('Europe/Copenhagen');
/**
* Check if the environment variables are set (If we are running in a Docker container)
*/
if (!isset($_ENV['USE_ENV']) || $_ENV['USE_ENV'] !== 'true') {
return;
}
$ENV_VARIABLES = [
'CONFIG_DB_HOST' => 'host',
'CONFIG_DB_USER' => 'user',
'CONFIG_DB_PASSWORD' => 'password',
'CONFIG_DB_DATABASE' => 'database',
'DEBUG' => 'DEBUG',
'ENCRYPTION_KEY' => 'ENCRYPTION_KEY',
'CORS' => 'CORS',
'ECONOMIC_API_APP_ACCESS_GRANT' => 'app_access_grant',
'ECONOMIC_API_APP_ACCESS_GRANT2' => 'app_access_grant2',
'ECONOMIC_API_APP_SECRET_TOKEN' => 'app_secret_token',
'WORDPRESS_STATIC_TOKEN' => 'WORDPRESS_STATIC_TOKEN',
'EMAIL_WASH_CERTIFICATE_TOKEN' => 'EMAIL_WASH_CERTIFICATE_TOKEN',
'WORDPRESS_API_URL' => 'WORDPRESS_API_URL',
'MINIO_ENDPOINT' => 'endpoint',
'MINIO_ACCESS_KEY' => 'access_key',
'MINIO_SECRET_KEY' => 'secret_key',
'SLACK_DEFAULT_WEBHOOK' => 'SLACK_DEFAULT_WEBHOOK',
'REDIS_CONFIG_HOST' => 'host',
'REDIS_CONFIG_DATABASE' => 'database',
'REDIS_CONFIG_PASSWORD' => 'password'
];
foreach ( $ENV_VARIABLES as $env => $config ) {
if (isset($_ENV[$env])) {
if ($config === 'DEBUG' || $config === 'USE_PROD_ECONOMIC_IN_DEBUG') {
$GLOBALS[$config] = $_ENV[$env] === 'true';
} else {
$GLOBALS[$config] = $_ENV[$env];
}
}
}
+73
View File
@@ -0,0 +1,73 @@
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: php_app
ports:
- "8080:80" # Map port 80 in the container to port 8080 on the host
volumes:
- .:/var/www/html # Mount the current directory to the container
- ./logs:/var/log/apache2 # Mount logs to be accessible on the host
environment:
# Pass environment variables for the application
DEBUG: true
USE_ENV: true
CONFIG_TIMEZONE: "Europe/Copenhagen" # Set the timezone
# Database configuration
CONFIG_DB_HOST: db
CONFIG_DB_USER: root
CONFIG_DB_PASSWORD: example_password
CONFIG_DB_DATABASE: example_db
# Security configuration
ENCRYPTION_KEY: "" # Set the encryption key
CORS: "*"
# Economic API credentials
ECONOMIC_API_APP_ACCESS_GRANT: "" # Economic API access grant token (1)
ECONOMIC_API_APP_ACCESS_GRANT2: "" # Economic API access grant token (2)
ECONOMIC_API_APP_SECRET_TOKEN: "" # Economic API secret token
# WordPress configuration
WORDPRESS_API_URL: "" # WordPress API URL (e.g. https://www.example.com/wp-admin/admin-ajax.php)
WORDPRESS_STATIC_TOKEN: "" # WordPress static token for authentication
EMAIL_WASH_CERTIFICATE_TOKEN: "" # Email wash certificate token
# MINIO configuration
MINIO_ENDPOINT: "" # Minio endpoint (e.g. http://localhost:9000)
MINIO_ACCESS_KEY: "" # Minio access key
MINIO_SECRET_KEY: "" # Minio secret key
# Redis configuration
REDIS_CONFIG_HOST: redis
REDIS_CONFIG_DATABASE: 0
REDIS_CONFIG_PASSWORD: ""
# Slack configuration
SLACK_DEFAULT_WEBHOOK: "" # Slack default webhook URL
depends_on:
- db
- redis
db:
image: mysql:8.0
container_name: mysql
ports:
- "3306:3306" # Map MySQL container's port 3306 to the host
volumes:
- db_data:/var/lib/mysql # Persist database data
environment:
MYSQL_ROOT_PASSWORD: example_password
MYSQL_DATABASE: example_db
MYSQL_USER: app_user
MYSQL_PASSWORD: app_password
redis:
image: redis:6.2
container_name: redis
ports:
- "6379:6379" # Map Redis container's port 6379 to the host
volumes:
- redis_data:/data # Persist Redis data
volumes:
db_data: # Persistent data for MySQL
redis_data: # Persistent data for Redis
@@ -10,7 +10,7 @@ class economic_invoice_draft_mo extends economicInvoicesDrafts
protected array $recipient; // Recipient of the invoice (Includes name, address, zip, city, and (array)vatZone)
protected array $lines; // Lines of the invoice (Includes product, quantity, unitNetPrice, discountPercentage, and (array)vatRate)
public function addLine(string $productNumber, string $description, float $quantity, float $unitNetPrice, float $discountPercentage, int $economic_department_id): void
public function addLine(string $productNumber, string $description, float $quantity, float $unitNetPrice, float $discountPercentage, int $economic_department_id, int $dimension): void
{
// We then add a 0 in front of the product number to make sure it's the right one, made by FlexPOS.
// Add a line to the invoice
@@ -27,7 +27,8 @@ class economic_invoice_draft_mo extends economicInvoicesDrafts
if ($economic_department_id) {
$line['departmentalDistribution'] = [
'departmentalDistributionNumber' => $economic_department_id,
'DistributionType' => 'Department'
'DistributionType' => 'Department',
'dimension' => $dimension
];
}
$this->lines[] = $line;
@@ -0,0 +1,401 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "Customer collection GET schema",
"description": "A schema for fetching a collection of customer, aka. Debtor.",
"type": "object",
"restdocs": "http://restdocs.e-conomic.com/#get-customers",
"properties": {
"collection": {
"type": "array",
"description": "A collection of customers.",
"items": {
"title": "Customer",
"type": "object",
"properties": {
"address": {
"type": "string",
"maxLength": 510,
"sortable": true,
"filterable": true,
"description": "Address for the customer including street and number."
},
"balance": {
"type": "number",
"readOnly": true,
"sortable": true,
"filterable": true,
"description": "The outstanding amount for this customer."
},
"barred": {
"type": "boolean",
"filterable": true,
"description": "Boolean indication of whether the customer is barred from invoicing."
},
"city": {
"type": "string",
"maxLength": 50,
"sortable": true,
"filterable": true,
"description": "The customer's city."
},
"corporateIdentificationNumber": {
"type": "string",
"maxLength": 40,
"sortable": true,
"filterable": true,
"description": "Corporate Identification Number. For example CVR in Denmark."
},
"pNumber": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"description": "Extension of corporate identification number (CVR). Identifying separate production unit (p-nummer)."
},
"country": {
"type": "string",
"maxLength": 50,
"sortable": true,
"filterable": true,
"description": "The customer's country."
},
"creditLimit": {
"type": "number",
"sortable": true,
"filterable": true,
"description": "A maximum credit for this customer. Once the maximum is reached or passed in connection with an order/quotation/invoice for this customer you see a warning in e-conomic."
},
"currency": {
"type": "string",
"maxLength": 3,
"minLength": 3,
"required": true,
"sortable": true,
"filterable": true,
"description": "Default payment currency."
},
"customerNumber": {
"type": "integer",
"maximum": 999999999,
"minimum": 1,
"sortable": true,
"filterable": true,
"description": "The customer number is a positive unique numerical identifier with a maximum of 9 digits."
},
"dueAmount": {
"type": "number",
"readOnly": true,
"sortable": false,
"filterable": false,
"description": "Due amount that the customer needs to pay."
},
"ean": {
"type": "string",
"maxLength": 13,
"sortable": true,
"filterable": true,
"description": "European Article Number. EAN is used for invoicing the Danish public sector."
},
"email": {
"type": "string",
"maxLength": 255,
"sortable": true,
"filterable": true,
"description": "Customer e-mail address where e-conomic invoices should be emailed. Note: you can specify multiple email addresses in this field, separated by a space. If you need to send a copy of the invoice or write to other e-mail addresses, you can also create one or more customer contacts."
},
"lastUpdated": {
"type": "string",
"format": "full-date",
"pattern": "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z",
"sortable": true,
"filterable": true,
"description": "The date this customer was last updated. The date is formatted according to ISO-8601."
},
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"required": true,
"sortable": true,
"filterable": true,
"description": "The customer name."
},
"publicEntryNumber": {
"type": "string",
"maxLength": 50,
"sortable": true,
"filterable": true,
"description": "The public entry number is used for electronic invoicing, to define the account invoices will be registered on at the customer."
},
"telephoneAndFaxNumber": {
"type": "string",
"maxLength": 255,
"sortable": true,
"filterable": true,
"description": "The customer's telephone and/or fax number."
},
"mobilePhone": {
"type": "string",
"maxLength": 50,
"sortable": true,
"filterable": true,
"description": "The customer's mobile phone number."
},
"eInvoicingDisabledByDefault": {
"type": "boolean",
"readonly": false,
"description": "Boolean indication of whether the default sending method should be email instead of e-invoice. This property is updatable only by using PATCH to /customers/:customerNumber"
},
"vatNumber": {
"type": "string",
"maxLength": 20,
"sortable": true,
"filterable": true,
"description": "The customer's value added tax identification number. This field is only available to agreements in Sweden, UK, Germany, Poland and Finland. Not to be mistaken for the danish CVR number, which is defined on the corporateIdentificationNumber property."
},
"website": {
"type": "string",
"maxLength": 255,
"sortable": true,
"filterable": true,
"description": "Customer website, if applicable."
},
"zip": {
"type": "string",
"maxLength": 30,
"sortable": true,
"filterable": true,
"description": "The customer's postcode."
},
"contacts": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the customer contacts items."
},
"deliveryLocations": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the customer delivery locations items."
},
"defaultDeliveryLocation": {
"type": "object",
"description": "Customers default delivery location.",
"properties": {
"deliveryLocationNumber": {
"type": "integer",
"description": "The unique identifier of the delivery location."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the delivery location.",
"required": true
}
}
},
"attention": {
"type": "object",
"description": "The customer's person of attention.",
"properties": {
"customerContactNumber": {
"type": "integer",
"description": "The unique identifier of the customer employee."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the customer employee item.",
"required": true
}
}
},
"customerContact": {
"type": "object",
"description": "Reference to main contact employee at customer.",
"properties": {
"customerContactNumber": {
"type": "integer",
"description": "The unique identifier of the customer contact."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the customer contact item.",
"required": true
}
}
},
"customerGroup": {
"type": "object",
"required": true,
"description": "Reference to the customer group this customer is attached to.",
"properties": {
"customerGroupNumber": {
"type": "integer",
"filterable": true,
"description": "The unique identifier of the customer group."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the customer group item.",
"required": true
}
}
},
"layout": {
"type": "object",
"description": "Layout to be applied for invoices and other documents for this customer.",
"properties": {
"layoutNumber": {
"type": "integer",
"description": "The unique identifier of the layout."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the layout item.",
"required": true
}
}
},
"paymentTerms": {
"type": "object",
"required": true,
"description": "The default payment terms for the customer.",
"properties": {
"paymentTermsNumber": {
"type": "integer",
"description": "The unique identifier of the payment terms."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the payment terms item.",
"required": true
}
}
},
"salesPerson": {
"type": "object",
"description": "Reference to the employee responsible for contact with this customer.",
"properties": {
"employeeNumber": {
"type": "integer",
"description": "The unique identifier of the employee."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the employee resource.",
"required": true
}
}
},
"vatZone": {
"type": "object",
"required": true,
"description": "Indicates in which VAT-zone the customer is located (e.g.: domestically, in Europe or elsewhere abroad).",
"properties": {
"vatZoneNumber": {
"type": "integer",
"description": "The unique identifier of the VAT-zone."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the VAT-zone item.",
"required": true
}
}
},
"templates": {
"type": "object",
"description": "",
"properties": {
"invoice": {
"type": "string",
"format": "uri",
"description": "The unique reference to the invoice template."
},
"invoiceLine": {
"type": "string",
"format": "uri",
"description": "The unique reference to the invoiceLine template."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the templates resource.",
"required": true
}
}
},
"totals": {
"type": "object",
"description": "",
"properties": {
"drafts": {
"type": "string",
"format": "uri",
"description": "The unique reference to the draft invoice totals for this customer."
},
"booked": {
"type": "string",
"format": "uri",
"description": "The unique reference to the booked invoice totals for this customer."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the totals resource for this customer.",
"required": true
}
}
},
"invoices": {
"type": "object",
"description": "",
"properties": {
"drafts": {
"type": "string",
"format": "uri",
"description": "The unique reference to the draft invoices for this customer."
},
"booked": {
"type": "string",
"format": "uri",
"description": "The unique reference to the booked invoices for this customer."
},
"self": {
"type": "string",
"format": "uri",
"description": "A unique link reference to the invoices resource for this customer.",
"required": true
}
}
},
"self": {
"type": "string",
"format": "uri",
"description": "The unique self reference of the customer resource.",
"required": true
}
}
}
},
"metaData": {
"type": "object",
"description": "Information about possible actions, endpoints and resource paths related to the endpoint."
},
"pagination": {
"type": "object",
"description": "Information about the pagination."
},
"self": {
"type": "string",
"format": "uri",
"description": "The unique self reference of the customer collection resource.",
"required": true
}
}
}
+16 -14
View File
@@ -236,19 +236,6 @@ class economicInvoiceRoute
});
}
function addOrderToInvoiceDraft(int $economic_invoice_draft_id, orders_o $order, users_o $customer, array $order_items): object
{
$economic_invoice_draft = (new economic_invoice_draft_mo());
// Add the department, date, reference
$this->addTheDepartmentDateReference($economic_invoice_draft, $order->getDepartmentByOrderId($order->id)['name'], $order);
// Add the lines to the invoice
foreach ( $order_items as $order_item ) {
// Add the order item to the invoice draft
$this->addOrderItemToInvoice($customer, $order, $order_item, $economic_invoice_draft, $order_item['quantity'] ?? 1);
}
return $economic_invoice_draft->addLinesToInvoiceDraft($economic_invoice_draft_id);
}
/**
* @param economic_invoice_draft_mo $economic_invoice_draft
* @param $department_name
@@ -327,6 +314,7 @@ class economicInvoiceRoute
// Get the department
$department = $order->getDepartmentByOrderId($order->id);
$economic_department_id = $department['economic_department_id'];
$economic_dimension_id = $department['economic_dimension_id'];
// Add the line to the invoice
$economic_invoice_draft->addLine(
@@ -335,7 +323,21 @@ class economicInvoiceRoute
(int)$quantity,
(int)$order_item['price'],
0, // Since we can't be specific about the discount, we set it to 0. (The API has a limit of 2 decimals, and that's not enough for our needs)
(int)$economic_department_id ?? 0
(int)$economic_department_id ?? 0,
(int)$economic_dimension_id ?? 0 // If the department is not set, we'll set it to 0
);
}
function addOrderToInvoiceDraft(int $economic_invoice_draft_id, orders_o $order, users_o $customer, array $order_items): object
{
$economic_invoice_draft = (new economic_invoice_draft_mo());
// Add the department, date, reference
$this->addTheDepartmentDateReference($economic_invoice_draft, $order->getDepartmentByOrderId($order->id)['name'], $order);
// Add the lines to the invoice
foreach ( $order_items as $order_item ) {
// Add the order item to the invoice draft
$this->addOrderItemToInvoice($customer, $order, $order_item, $economic_invoice_draft, $order_item['quantity'] ?? 1);
}
return $economic_invoice_draft->addLinesToInvoiceDraft($economic_invoice_draft_id);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace routes;
use traits\route_t;
class workerRoute
{
use route_t;
public function run(): void
{
$this->get('/worker/status', function () {
global $response;
$response->success(['message' => 'Worker is running', 'status' => 'OK', 'time' => date('Y-m-d H:i:s'), 'timezone' => date_default_timezone_get()]);
});
}
}