Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 556
0.00% covered (danger)
0.00%
0 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiBillController
0.00% covered (danger)
0.00%
0 / 556
0.00% covered (danger)
0.00%
0 / 34
9312
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 apiBillUpdate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 validateBillUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 updateBillFromRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 apiBillCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 createBillDatabaseEntry
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 createBaseBill
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
156
 createBaseBillElement
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 createBillFromRequest
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 validateBillCreate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 apiMediaAddToBill
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 1
90
 apiMediaRemoveFromBill
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
30
 validateMediaRemoveFromBill
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 createBillDir
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 validateMediaAddToBill
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 apiBillElementCreate
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 createBillElementFromRequest
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 validateBillElementCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiMediaRender
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 apiPreviewRender
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
30
 apiBillPdfArchiveCreate
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 1
72
 sendBillEmail
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 apiNoteCreate
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 validateNoteCreate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiBillDelete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 deleteBillFromRequest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validateBillDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiBillElementUpdate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 updateBillElementFromRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateBillElementUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiBillElementDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateBillElementDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 apiNoteUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 apiNoteDelete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   Modules\Billing
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace Modules\Billing\Controller;
16
17use Modules\Admin\Models\NullAccount;
18use Modules\Admin\Models\SettingsEnum as AdminSettingsEnum;
19use Modules\Billing\Models\Bill;
20use Modules\Billing\Models\BillElement;
21use Modules\Billing\Models\BillElementMapper;
22use Modules\Billing\Models\BillMapper;
23use Modules\Billing\Models\BillStatus;
24use Modules\Billing\Models\BillTypeMapper;
25use Modules\Billing\Models\NullBill;
26use Modules\Billing\Models\NullBillElement;
27use Modules\Billing\Models\SettingsEnum;
28use Modules\ClientManagement\Models\Client;
29use Modules\ClientManagement\Models\ClientMapper;
30use Modules\ItemManagement\Models\Item;
31use Modules\ItemManagement\Models\ItemMapper;
32use Modules\Media\Models\CollectionMapper;
33use Modules\Media\Models\Media;
34use Modules\Media\Models\MediaMapper;
35use Modules\Media\Models\PathSettings;
36use Modules\Media\Models\UploadStatus;
37use Modules\Messages\Models\EmailMapper;
38use Modules\SupplierManagement\Models\NullSupplier;
39use Modules\SupplierManagement\Models\Supplier;
40use Modules\SupplierManagement\Models\SupplierMapper;
41use phpOMS\Application\ApplicationAbstract;
42use phpOMS\Autoloader;
43use phpOMS\Localization\ISO4217CharEnum;
44use phpOMS\Localization\ISO639x1Enum;
45use phpOMS\Message\Http\RequestStatusCode;
46use phpOMS\Message\Mail\Email;
47use phpOMS\Message\NotificationLevel;
48use phpOMS\Message\RequestAbstract;
49use phpOMS\Message\ResponseAbstract;
50use phpOMS\Model\Message\FormValidation;
51use phpOMS\System\MimeType;
52use phpOMS\Views\View;
53
54/**
55 * Billing class.
56 *
57 * @package Modules\Billing
58 * @license OMS License 2.0
59 * @link    https://jingga.app
60 * @since   1.0.0
61 */
62final class ApiBillController extends Controller
63{
64    /**
65     * Constructor.
66     *
67     * @param null|ApplicationAbstract $app Application instance
68     *
69     * @since 1.0.0
70     */
71    public function __construct(ApplicationAbstract $app = null)
72    {
73        parent::__construct($app);
74
75        if ($this->app->moduleManager->isActive('WarehouseManagement')) {
76            $this->app->eventManager->importFromFile(__DIR__ . '/../../WarehouseManagement/Admin/Hooks/Manual.php');
77        }
78    }
79
80    /**
81     * Api method to update a bill
82     *
83     * @param RequestAbstract  $request  Request
84     * @param ResponseAbstract $response Response
85     * @param array            $data     Generic data
86     *
87     * @return void
88     *
89     * @api
90     *
91     * @since 1.0.0
92     */
93    public function apiBillUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
94    {
95        if (!empty($val = $this->validateBillUpdate($request))) {
96            $response->header->status = RequestStatusCode::R_400;
97            $this->createInvalidUpdateResponse($request, $response, $val);
98
99            return;
100        }
101
102        /** @var \Modules\Billing\Models\Bill $old */
103        $old = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute();
104        $new = $this->updateBillFromRequest($request, clone $old);
105
106        $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin());
107        $this->createStandardUpdateResponse($request, $response, $new);
108    }
109
110    /**
111     * Method to validate bill creation from request
112     *
113     * @param RequestAbstract $request Request
114     *
115     * @return array<string, bool>
116     *
117     * @since 1.0.0
118     */
119    private function validateBillUpdate(RequestAbstract $request) : array
120    {
121        $val = [];
122        if (($val['bill'] = !$request->hasData('bill'))) {
123            return $val;
124        }
125
126        return [];
127    }
128
129    /**
130     * Method to create a bill from request.
131     *
132     * @param RequestAbstract $request Request
133     * @param Bill            $old     Bill
134     *
135     * @return Bill
136     *
137     * @since 1.0.0
138     */
139    public function updateBillFromRequest(RequestAbstract $request, Bill $old) : Bill
140    {
141        return $old;
142    }
143
144    /**
145     * Api method to create a bill
146     *
147     * @param RequestAbstract  $request  Request
148     * @param ResponseAbstract $response Response
149     * @param array            $data     Generic data
150     *
151     * @return void
152     *
153     * @api
154     *
155     * @since 1.0.0
156     */
157    public function apiBillCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
158    {
159        if (!empty($val = $this->validateBillCreate($request))) {
160            $response->header->status = RequestStatusCode::R_400;
161            $this->createInvalidCreateResponse($request, $response, $val);
162
163            return;
164        }
165
166        $bill = $this->createBillFromRequest($request, $response, $data);
167        $this->createBillDatabaseEntry($bill, $request);
168        $this->createStandardCreateResponse($request, $response, $bill);
169    }
170
171    /**
172     * Create a new database entry for a Bill object and update its bill number
173     *
174     * @param Bill            $bill    The Bill object to create a database entry for and update its bill number
175     * @param RequestAbstract $request The request object that contains the header account and origin
176     *
177     * @return void
178     *
179     * @since 1.0.0
180     */
181    public function createBillDatabaseEntry(Bill $bill, RequestAbstract $request) : void
182    {
183        $this->createModel($request->header->account, $bill, BillMapper::class, 'bill', $request->getOrigin());
184
185        // We ned to get the bill again since the bill has a trigger which is executed on insert
186        // @todo: consider to remove the trigger and select the latest bill here and add + 1 to the new sequence since we have to tdo an update anyways
187        /** @var Bill $bill */
188        $tmp = BillMapper::get()
189            ->where('id', $bill->id)
190            ->execute();
191
192        $bill->sequence = $tmp->sequence;
193
194        $old = clone $bill;
195        $bill->buildNumber(); // The bill id is part of the number
196        $this->updateModel($request->header->account, $old, $bill, BillMapper::class, 'bill', $request->getOrigin());
197    }
198
199    /**
200     * Create a base Bill object with default values
201     *
202     * @param Client|Supplier $account The client or supplier object for whom the bill is being created
203     * @param RequestAbstract $request The request object that contains the header account
204     *
205     * @return Bill The new Bill object with default values
206     *
207     * @todo Validate VAT before creation (maybe need to add a status when last validated, we don't want to validate every time)
208     * @todo Set the correct date of payment
209     * @todo Use bill and shipping address instead of main address if available
210     * @todo Implement allowed invoice languages and a default invoice language if none match
211     * @todo Implement client invoice language (allowing for different invoice languages than invoice address)
212     *
213     * @since 1.0.0
214     */
215    public function createBaseBill(Client | Supplier $account, RequestAbstract $request) : Bill
216    {
217        // @todo: validate vat before creation for clients
218        $bill                  = new Bill();
219        $bill->createdBy       = new NullAccount($request->header->account);
220        $bill->unit            = $account->unit ?? $this->app->unitId;
221        $bill->billDate        = new \DateTime('now'); // @todo: Date of payment
222        $bill->performanceDate = $request->getDataDateTime('performancedate') ?? new \DateTime('now'); // @todo: Date of payment
223        $bill->accountNumber   = $account->number;
224        $bill->setStatus($request->getDataInt('status') ?? BillStatus::DRAFT);
225
226        $bill->shipping     = 0;
227        $bill->shippingText = '';
228
229        $bill->payment     = 0;
230        $bill->paymentText = '';
231
232        if ($account instanceof Client) {
233            $bill->client = $account;
234        } else {
235            $bill->supplier = $account;
236        }
237
238        // @todo: use bill and shipping address instead of main address if available
239        $bill->billTo      = $request->getDataString('billto') ?? $account->account->name1;
240        $bill->billAddress = $request->getDataString('billaddress') ?? $account->mainAddress->address;
241        $bill->billCity    = $request->getDataString('billtocity') ?? $account->mainAddress->city;
242        $bill->billZip     = $request->getDataString('billtopostal') ?? $account->mainAddress->postal;
243        $bill->billCountry = $request->getDataString('billtocountry') ?? $account->mainAddress->getCountry();
244
245        $bill->setCurrency(ISO4217CharEnum::_EUR);
246
247        /** @var \Model\Setting $settings */
248        $settings = $this->app->appSettings->get(null,
249            SettingsEnum::VALID_BILL_LANGUAGES,
250            unit: $this->app->unitId,
251            module: 'Admin'
252        );
253
254        if (empty($settings)) {
255            /** @var \Model\Setting $settings */
256            $settings = $this->app->appSettings->get(null,
257            SettingsEnum::VALID_BILL_LANGUAGES,
258                unit: null,
259                module: 'Admin'
260            );
261        }
262
263        $validLanguages = [];
264        if (!empty($settings) && !empty($settings->content)) {
265            $validLanguages = \json_decode($settings->content, true);
266        }
267
268        if (empty($validLanguages) || !\is_array($validLanguages)) {
269            $validLanguages = [
270                ISO639x1Enum::_EN,
271            ];
272        }
273
274        $billLanguage = $validLanguages[0] ?? ISO639x1Enum::_EN;
275
276        $accountBillLanguage = $account->getAttribute('bill_language')->value->valueStr;
277        if (!empty($accountBillLanguage) && \in_array($accountBillLanguage, $validLanguages)) {
278            $billLanguage = $accountBillLanguage;
279        } else {
280            $accountLanguages = ISO639x1Enum::languageFromCountry($account->mainAddress->getCountry());
281            $accountLanguage  = empty($accountLanguages) ? '' : $accountLanguages[0];
282
283            if (\in_array($accountLanguage, $validLanguages)) {
284                $billLanguage = $accountLanguage;
285            }
286        }
287
288        $bill->language = $billLanguage;
289
290        $typeMapper = BillTypeMapper::get()
291            ->with('l11n')
292            ->where('l11n/langauge', $billLanguage)
293            ->limit(1);
294
295        if ($request->hasData('type')) {
296            $typeMapper->where('id', $request->getDataInt('type'));
297        } else {
298            $typeMapper->where('name', 'sales_invoice');
299        }
300
301        $bill->type = $typeMapper->execute();
302
303        return $bill;
304    }
305
306    /**
307     * Create a base BillElement object with default values
308     *
309     * @param Client          $client  The client object for whom the bill is being created
310     * @param Item            $item    The item object for which the bill element is being created
311     * @param Bill            $bill    The bill object for which the bill element is being created
312     * @param RequestAbstract $request The request object that contains the header account
313     *
314     * @return BillElement
315     *
316     * @since 1.0.0
317     */
318    public function createBaseBillElement(Client $client, Item $item, Bill $bill, RequestAbstract $request) : BillElement
319    {
320        $taxCode = $this->app->moduleManager->get('Billing', 'ApiTax')
321            ->getTaxCodeFromClientItem($client, $item, $request->header->l11n->country);
322
323        return BillElement::fromItem(
324            $item,
325            $taxCode,
326            $request->getDataInt('quantity') ?? 1,
327            $bill->id
328        );
329    }
330
331    /**
332     * Method to create a bill from request.
333     *
334     * @param RequestAbstract  $request  Request
335     * @param ResponseAbstract $response Response
336     * @param array            $data     Generic data
337     *
338     * @return Bill
339     *
340     * @since 1.0.0
341     */
342    public function createBillFromRequest(RequestAbstract $request, ResponseAbstract $response, $data = null) : Bill
343    {
344        /** @var \Modules\ClientManagement\Models\Client|\Modules\SupplierManagement\Models\Supplier $account */
345        $account = null;
346        if ($request->hasData('client')) {
347            /** @var \Modules\ClientManagement\Models\Client $account */
348            $account = ClientMapper::get()
349                ->with('account')
350                ->with('mainAddress')
351                ->where('id', (int) $request->getData('client'))
352                ->execute();
353        } elseif (($request->getDataInt('supplier') ?? -1) === 0) {
354            /** @var \Modules\SupplierManagement\Models\Supplier $account */
355            $account = new NullSupplier();
356        } elseif ($request->hasData('supplier')) {
357            /** @var \Modules\SupplierManagement\Models\Supplier $account */
358            $account = SupplierMapper::get()
359                ->with('account')
360                ->with('mainAddress')
361                ->where('id', (int) $request->getData('supplier'))
362                ->execute();
363        }
364
365        return $this->createBaseBill($account, $request);
366    }
367
368    /**
369     * Method to validate bill creation from request
370     *
371     * @param RequestAbstract $request Request
372     *
373     * @return array<string, bool>
374     *
375     * @since 1.0.0
376     */
377    private function validateBillCreate(RequestAbstract $request) : array
378    {
379        $val = [];
380        if (($val['client/supplier'] = (!$request->hasData('client')
381                && (!$request->hasData('supplier')
382                    && ($request->getDataInt('supplier') ?? -1) !== 0)
383                ))
384            || ($val['type'] = (!$request->hasData('type')))
385        ) {
386            return $val;
387        }
388
389        return [];
390    }
391
392    /**
393     * Api method to add Media to a Bill
394     *
395     * @param RequestAbstract  $request  Request
396     * @param ResponseAbstract $response Response
397     * @param array            $data     Generic data
398     *
399     * @return void
400     *
401     * @api
402     *
403     * @since 1.0.0
404     */
405    public function apiMediaAddToBill(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
406    {
407        if (!empty($val = $this->validateMediaAddToBill($request))) {
408            $response->header->status = RequestStatusCode::R_400;
409            $this->createInvalidUpdateResponse($request, $response, $val);
410
411            return;
412        }
413
414        /** @var \Modules\Billing\Models\Bill $bill */
415        $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute();
416        $path = $this->createBillDir($bill);
417
418        $uploaded = [];
419        if (!empty($uploadedFiles = $request->files)) {
420            $uploaded = $this->app->moduleManager->get('Media')->uploadFiles(
421                names: [],
422                fileNames: [],
423                files: $uploadedFiles,
424                account: $request->header->account,
425                basePath: __DIR__ . '/../../../Modules/Media/Files' . $path,
426                virtualPath: $path,
427                pathSettings: PathSettings::FILE_PATH,
428                hasAccountRelation: false,
429                readContent: $request->getDataBool('parse_content') ?? false
430            );
431
432            $collection = null;
433            foreach ($uploaded as $media) {
434                $this->createModelRelation(
435                    $request->header->account,
436                    $bill->id,
437                    $media->id,
438                    BillMapper::class,
439                    'files',
440                    '',
441                    $request->getOrigin()
442                );
443
444                if ($request->hasData('type')) {
445                    $this->createModelRelation(
446                        $request->header->account,
447                        $media->id,
448                        $request->getDataInt('type'),
449                        MediaMapper::class,
450                        'types',
451                        '',
452                        $request->getOrigin()
453                    );
454                }
455
456                if ($collection === null) {
457                    /** @var \Modules\Media\Models\Collection $collection */
458                    $collection = MediaMapper::getParentCollection($path)
459                        ->limit(1)
460                        ->execute();
461
462                    if ($collection->id === 0) {
463                        $collection = $this->app->moduleManager->get('Media')->createRecursiveMediaCollection(
464                            $path,
465                            $request->header->account,
466                            __DIR__ . '/../../../Modules/Media/Files' . $path,
467                        );
468                    }
469                }
470
471                $this->createModelRelation(
472                    $request->header->account,
473                    $collection->id,
474                    $media->id,
475                    CollectionMapper::class,
476                    'sources',
477                    '',
478                    $request->getOrigin()
479                );
480            }
481        }
482
483        if (!empty($mediaFiles = $request->getDataJson('media'))) {
484            foreach ($mediaFiles as $media) {
485                $this->createModelRelation(
486                    $request->header->account,
487                    $bill->id,
488                    (int) $media,
489                    BillMapper::class,
490                    'files',
491                    '',
492                    $request->getOrigin()
493                );
494            }
495        }
496
497        $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media added to bill.', [
498            'upload' => $uploaded,
499            'media'  => $mediaFiles,
500        ]);
501    }
502
503    /**
504     * Api method to remove Media from Bill
505     *
506     * @param RequestAbstract  $request  Request
507     * @param ResponseAbstract $response Response
508     * @param array            $data     Generic data
509     *
510     * @return void
511     *
512     * @api
513     *
514     * @since 1.0.0
515     */
516    public function apiMediaRemoveFromBill(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
517    {
518        // @todo: check that it is not system generated media!
519        if (!empty($val = $this->validateMediaRemoveFromBill($request))) {
520            $response->header->status = RequestStatusCode::R_400;
521            $this->createInvalidDeleteResponse($request, $response, $val);
522
523            return;
524        }
525
526        /** @var \Modules\Media\Models\Media $media */
527        $media = MediaMapper::get()->where('id', (int) $request->getData('media'))->execute();
528
529        /** @var \Modules\Billing\Models\Bill $bill */
530        $bill = BillMapper::get()->where('id', (int) $request->getData('bill'))->execute();
531
532        $path = $this->createBillDir($bill);
533
534        /** @var \Modules\Media\Models\Collection[] */
535        $billCollection = CollectionMapper::getAll()
536            ->where('virtual', $path)
537            ->execute();
538
539        if (\count($billCollection) !== 1) {
540            // For some reason there are multiple collections with the same virtual path?
541            // @todo: check if this is the correct way to handle it or if we need to make sure that it is a collection
542            return;
543        }
544
545        $collection = \reset($billCollection);
546
547        $this->deleteModelRelation(
548            $request->header->account,
549            $bill->id,
550            $media->id,
551            BillMapper::class,
552            'files',
553            '',
554            $request->getOrigin()
555        );
556
557        $this->deleteModelRelation(
558            $request->header->account,
559            $collection->id,
560            $media->id,
561            CollectionMapper::class,
562            'sources',
563            '',
564            $request->getOrigin()
565        );
566
567        $referenceCount = MediaMapper::countInternalReferences($media->id);
568
569        if ($referenceCount === 0) {
570            // Is not used anywhere else -> remove from db and file system
571
572            // @todo: remove media types from media
573
574            $this->deleteModel($request->header->account, $media, MediaMapper::class, 'bill_media', $request->getOrigin());
575
576            if (\is_dir($media->getAbsolutePath())) {
577                \phpOMS\System\File\Local\Directory::delete($media->getAbsolutePath());
578            } else {
579                \phpOMS\System\File\Local\File::delete($media->getAbsolutePath());
580            }
581        }
582
583        $this->createStandardDeleteResponse($request, $response, $media);
584    }
585
586    /**
587     * Validate Media remove from Bill request
588     *
589     * @param RequestAbstract $request Request
590     *
591     * @return array<string, bool>
592     *
593     * @since 1.0.0
594     */
595    private function validateMediaRemoveFromBill(RequestAbstract $request) : array
596    {
597        $val = [];
598        if (($val['media'] = !$request->hasData('media'))
599            || ($val['bill'] = !$request->hasData('bill'))
600        ) {
601            return $val;
602        }
603
604        return [];
605    }
606
607    /**
608     * Create media directory path
609     *
610     * @param Bill $bill Bill
611     *
612     * @return string
613     *
614     * @since 1.0.0
615     */
616    private function createBillDir(Bill $bill) : string
617    {
618        return '/Modules/Billing/Bills/'
619            . $this->app->unitId . '/'
620            . $bill->createdAt->format('Y/m/d') . '/'
621            . $bill->id;
622    }
623
624    /**
625     * Method to validate bill creation from request
626     *
627     * @param RequestAbstract $request Request
628     *
629     * @return array<string, bool>
630     *
631     * @since 1.0.0
632     */
633    private function validateMediaAddToBill(RequestAbstract $request) : array
634    {
635        $val = [];
636        if (($val['media'] = (!$request->hasData('media') && empty($request->files)))
637            || ($val['bill'] = !$request->hasData('bill'))
638        ) {
639            return $val;
640        }
641
642        return [];
643    }
644
645    /**
646     * Api method to create a bill element
647     *
648     * @param RequestAbstract  $request  Request
649     * @param ResponseAbstract $response Response
650     * @param array            $data     Generic data
651     *
652     * @return void
653     *
654     * @api
655     *
656     * @since 1.0.0
657     */
658    public function apiBillElementCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
659    {
660        if (!empty($val = $this->validateBillElementCreate($request))) {
661            $response->header->status = RequestStatusCode::R_400;
662            $this->createInvalidCreateResponse($request, $response, $val);
663
664            return;
665        }
666
667        /** @var \Modules\Billing\Models\Bill $old */
668        $old = BillMapper::get()
669            ->with('client')
670            ->with('client/attributes')
671            ->with('client/attributes/type')
672            ->with('client/attributes/value')
673            ->where('id', $request->getDataInt('bill') ?? 0)
674            ->execute();
675
676        $element = $this->createBillElementFromRequest($request, $response, $old, $data);
677        $this->createModel($request->header->account, $element, BillElementMapper::class, 'bill_element', $request->getOrigin());
678
679        // @todo: handle stock transaction here
680        // @todo: if transaction fails don't update below and send warning to user
681        // @todo: however mark transaction as reserved and only update when bill is finalized!!!
682
683        // @todo: in BillElementUpdate do the same
684
685        $new = clone $old;
686        $new->addElement($element);
687
688        $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill_element', $request->getOrigin());
689        $this->createStandardCreateResponse($request, $response, $element);
690    }
691
692    /**
693     * Method to create a bill element from request.
694     *
695     * @param RequestAbstract  $request  Request
696     * @param ResponseAbstract $response Response
697     * @param Bill             $bill     Bill to create element for
698     * @param array            $data     Generic data
699     *
700     * @return BillElement
701     *
702     * @since 1.0.0
703     */
704    private function createBillElementFromRequest(RequestAbstract $request, ResponseAbstract $response, Bill $bill, $data = null) : BillElement
705    {
706        /** @var \Modules\ItemManagement\Models\Item $item */
707        $item = ItemMapper::get()
708            ->with('attributes')
709            ->with('attributes/type')
710            ->with('attributes/value')
711            ->with('l11n')
712            ->with('l11n/type')
713            ->where('id', $request->getDataInt('item') ?? 0)
714            ->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN')
715            ->where('l11n/language', $bill->language)
716            ->execute();
717
718        if ($bill->client === null) {
719            return new NullBillElement();
720        }
721
722        $element       = $this->createBaseBillElement($bill->client, $item, $bill, $request);
723        $element->bill = new NullBill($bill->id);
724
725        // discounts
726        // @todo: implement a addDiscount function
727        /*
728        if ($request->getData('discount_percentage') !== null) {
729        }
730        */
731
732        return $element;
733    }
734
735    /**
736     * Method to validate bill element creation from request
737     *
738     * @param RequestAbstract $request Request
739     *
740     * @return array<string, bool>
741     *
742     * @since 1.0.0
743     */
744    private function validateBillElementCreate(RequestAbstract $request) : array
745    {
746        $val = [];
747        if (($val['bill'] = !$request->hasData('bill'))) {
748            return $val;
749        }
750
751        return [];
752    }
753
754    /**
755     * Render bill media
756     *
757     * @param RequestAbstract  $request  Request
758     * @param ResponseAbstract $response Response
759     * @param array            $data     Generic data
760     *
761     * @return void
762     *
763     * @api
764     *
765     * @since 1.0.0
766     */
767    public function apiMediaRender(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
768    {
769        // @todo: check if has permission
770        $this->app->moduleManager->get('Media', 'Api')->apiMediaExport($request, $response, ['ignorePermission' => true]);
771    }
772
773    /**
774     * Api method  to create a bill preview
775     *
776     * @param RequestAbstract  $request  Request
777     * @param ResponseAbstract $response Response
778     * @param array            $data     Generic data
779     *
780     * @return void
781     *
782     * @api
783     *
784     * @since 1.0.0
785     */
786    public function apiPreviewRender(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
787    {
788        /** @var \Modules\Billing\Models\Bill $bill */
789        $bill = BillMapper::get()
790            ->with('type')
791            ->with('type/l11n')
792            ->with('elements')
793            ->where('id', $request->getDataInt('bill') ?? 0)
794            ->execute();
795
796        Autoloader::addPath(__DIR__ . '/../../../Resources/');
797
798        $templateId = $request->getData('bill_template', 'int');
799        if ($templateId === null) {
800            $billTypeId = $request->getData('bill_type', 'int');
801
802            if (empty($billTypeId)) {
803                $billTypeId = $bill->type->id;
804            }
805
806            if (empty($billTypeId)) {
807                return;
808            }
809
810            /** @var \Modules\Billing\Models\BillType $billType */
811            $billType = BillTypeMapper::get()
812                ->with('defaultTemplate')
813                ->where('id', $billTypeId)
814                ->execute();
815
816            $templateId = $billType->defaultTemplate?->id;
817        }
818
819        /** @var \Modules\Media\Models\Collection $template */
820        $template = CollectionMapper::get()
821            ->with('sources')
822            ->where('id', $templateId)
823            ->execute();
824
825        require_once __DIR__ . '/../../../Resources/tcpdf/TCPDF.php';
826
827        $response->header->set('Content-Type', MimeType::M_PDF, true);
828
829        $view = new View($this->app->l11nManager, $request, $response);
830        $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php');
831
832        /** @var \Model\Setting[] $settings */
833        $settings = $this->app->appSettings->get(null,
834            [
835                AdminSettingsEnum::DEFAULT_TEMPLATES,
836                AdminSettingsEnum::DEFAULT_ASSETS,
837            ],
838            unit: $this->app->unitId,
839            module: 'Admin'
840        );
841
842        if (empty($settings)) {
843            /** @var \Model\Setting[] $settings */
844            $settings = $this->app->appSettings->get(null,
845                [
846                    AdminSettingsEnum::DEFAULT_TEMPLATES,
847                    AdminSettingsEnum::DEFAULT_ASSETS,
848                ],
849                unit: null,
850                module: 'Admin'
851            );
852        }
853
854        /** @var \Modules\Media\Models\Collection $defaultTemplates */
855        $defaultTemplates = CollectionMapper::get()
856            ->with('sources')
857            ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content)
858            ->execute();
859
860        /** @var \Modules\Media\Models\Collection $defaultAssets */
861        $defaultAssets = CollectionMapper::get()
862            ->with('sources')
863            ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content)
864            ->execute();
865
866        $view->data['defaultTemplates'] = $defaultTemplates;
867        $view->data['defaultAssets']    = $defaultAssets;
868
869        $path   = $this->createBillDir($bill);
870        $pdfDir = __DIR__ . '/../../../Modules/Media/Files' . $path;
871
872        $view->data['bill'] = $bill;
873        $view->data['path'] = $pdfDir . '/' . ($bill->billDate?->format('Y-m-d') ?? '0') . '_' . $bill->number . '.pdf';
874
875        $view->data['bill_creator']  = $request->getDataString('bill_creator');
876        $view->data['bill_title']    = $request->getDataString('bill_title');
877        $view->data['bill_subtitle'] = $request->getDataString('bill_subtitle');
878        $view->data['keywords']      = $request->getDataString('keywords');
879
880        $view->data['bill_type_name'] = $request->getDataString('bill_type_name');
881
882        $view->data['bill_start_text'] = $request->getDataString('bill_start_text');
883        $view->data['bill_lines']      = $request->getDataString('bill_lines');
884        $view->data['bill_end_text']   = $request->getDataString('bill_end_text');
885
886        $view->data['bill_payment_terms'] = $request->getDataString('bill_payment_terms');
887        $view->data['bill_terms']         = $request->getDataString('bill_terms');
888        $view->data['bill_taxes']         = $request->getDataString('bill_taxes');
889        $view->data['bill_currency']      = $request->getDataString('bill_currency');
890
891        // Unit specifc settings
892        $view->data['bill_logo_name']       = $request->getDataString('bill_logo_name');
893        $view->data['bill_slogan']          = $request->getDataString('bill_slogan');
894        $view->data['legal_company_name']   = $request->getDataString('legal_company_name');
895        $view->data['bill_company_address'] = $request->getDataString('bill_company_address');
896        $view->data['bill_company_city']    = $request->getDataString('bill_company_city');
897        $view->data['bill_company_ceo']     = $request->getDataString('bill_company_ceo');
898        $view->data['bill_company_website'] = $request->getDataString('bill_company_website');
899        $view->data['bill_company_email']   = $request->getDataString('bill_company_email');
900        $view->data['bill_company_phone']   = $request->getDataString('bill_company_phone');
901        $view->data['bill_company_terms']   = $request->getDataString('bill_company_terms');
902
903        $view->data['bill_company_tax_office'] = $request->getDataString('bill_company_tax_office');
904        $view->data['bill_company_tax_id']     = $request->getDataString('bill_company_tax_id');
905        $view->data['bill_company_vat_id']     = $request->getDataString('bill_company_vat_id');
906
907        $view->data['bill_company_bank_name']    = $request->getDataString('bill_company_bank_name');
908        $view->data['bill_company_swift']        = $request->getDataString('bill_company_swift');
909        $view->data['bill_company_bank_account'] = $request->getDataString('bill_company_bank_account');
910
911        $pdf = $view->render();
912
913        $response->set('', $pdf);
914    }
915
916    /**
917     * Api method to create and archive a bill
918     *
919     * @param RequestAbstract  $request  Request
920     * @param ResponseAbstract $response Response
921     * @param array            $data     Generic data
922     *
923     * @return void
924     *
925     * @api
926     *
927     * @since 1.0.0
928     */
929    public function apiBillPdfArchiveCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
930    {
931        Autoloader::addPath(__DIR__ . '/../../../Resources/');
932
933        /** @var \Modules\Billing\Models\Bill $bill */
934        $bill = BillMapper::get()
935            ->where('id', $request->getDataInt('bill') ?? 0)
936            ->execute();
937
938        // @todo: This is stupid to do twice but I need to get the langauge.
939        // For the future it should just be a join on the bill langauge!!!
940        // The problem is the where here is a model where and not a query
941        // builder where meaning it is always considered a value and not a column.
942
943        /** @var \Modules\Billing\Models\Bill $bill */
944        $bill = BillMapper::get()
945            ->with('type')
946            ->with('type/l11n')
947            ->with('type/defaultTemplate')
948            ->with('elements')
949            ->where('id', $request->getDataInt('bill') ?? 0)
950            ->where('type/l11n/language', $bill->language)
951            ->execute();
952
953        $templateId = $request->getDataInt('bill_template');
954        if ($templateId === null) {
955            $templateId = $bill->type->defaultTemplate?->id;
956        }
957
958        /** @var \Modules\Media\Models\Collection $template */
959        $template = CollectionMapper::get()
960            ->with('sources')
961            ->where('id', $templateId)
962            ->execute();
963
964        require_once __DIR__ . '/../../../Resources/tcpdf/TCPDF.php';
965
966        $view = new View($this->app->l11nManager, $request, $response);
967        $view->setTemplate('/' . \substr($template->getSourceByName('bill.pdf.php')->getPath(), 0, -8), 'pdf.php');
968
969        /** @var \Model\Setting[] $settings */
970        $settings = $this->app->appSettings->get(null,
971            [
972                AdminSettingsEnum::DEFAULT_TEMPLATES,
973                AdminSettingsEnum::DEFAULT_ASSETS,
974            ],
975            unit: $this->app->unitId,
976            module: 'Admin'
977        );
978
979        if (empty($settings)) {
980            /** @var \Model\Setting[] $settings */
981            $settings = $this->app->appSettings->get(null,
982                [
983                    AdminSettingsEnum::DEFAULT_TEMPLATES,
984                    AdminSettingsEnum::DEFAULT_ASSETS,
985                ],
986                unit: null,
987                module: 'Admin'
988            );
989        }
990
991        /** @var \Modules\Media\Models\Collection $defaultTemplates */
992        $defaultTemplates = CollectionMapper::get()
993            ->with('sources')
994            ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_TEMPLATES]->content)
995            ->execute();
996
997        /** @var \Modules\Media\Models\Collection $defaultAssets */
998        $defaultAssets = CollectionMapper::get()
999            ->with('sources')
1000            ->where('id', (int) $settings[AdminSettingsEnum::DEFAULT_ASSETS]->content)
1001            ->execute();
1002
1003        $view->data['defaultTemplates'] = $defaultTemplates;
1004        $view->data['defaultAssets']    = $defaultAssets;
1005        $view->data['bill']             = $bill;
1006
1007        // @todo: add bill data such as company name bank information, ..., etc.
1008
1009        $pdf = $view->render();
1010
1011        $path   = $this->createBillDir($bill);
1012        $pdfDir = __DIR__ . '/../../../Modules/Media/Files' . $path;
1013
1014        $status = \is_dir($pdfDir) ? true : \mkdir($pdfDir, 0755, true);
1015        if (!$status) {
1016            // @codeCoverageIgnoreStart
1017            $response->set($request->uri->__toString(), new FormValidation(['status' => $status]));
1018            $response->header->status = RequestStatusCode::R_400;
1019
1020            return;
1021            // @codeCoverageIgnoreEnd
1022        }
1023
1024        $billFileName = ($bill->billDate?->format('Y-m-d') ?? '0') . '_' . $bill->number . '.pdf';
1025
1026        \file_put_contents($pdfDir . '/' . $billFileName, $pdf);
1027        if (!\is_file($pdfDir . '/' . $billFileName)) {
1028            $response->header->status = RequestStatusCode::R_400;
1029
1030            return;
1031        }
1032
1033        $media = $this->app->moduleManager->get('Media', 'Api')->createDbEntry(
1034            status: [
1035                'status'    => UploadStatus::OK,
1036                'name'      => $billFileName,
1037                'path'      => $pdfDir,
1038                'filename'  => $billFileName,
1039                'size'      => \filesize($pdfDir . '/' . $billFileName),
1040                'extension' => 'pdf',
1041            ],
1042            account: $request->header->account,
1043            virtualPath: $path,
1044            ip: $request->getOrigin(),
1045            app: $this->app,
1046            readContent: true,
1047            unit: $this->app->unitId
1048        );
1049
1050        // Send bill via email
1051        // @todo: maybe not all bill types, and bill status (e.g. deleted should not be sent)
1052        $client = ClientMapper::get()
1053            ->with('account')
1054            ->with('attributes')
1055            ->with('attributes/type')
1056            ->with('attributes/value')
1057            ->where('id', $bill->client?->id ?? 0)
1058            ->where('attributes/type/name', ['bill_emails', 'bill_email_address'], 'IN')
1059            ->execute();
1060
1061        if ($client->getAttribute('bill_emails')->value->getValue() === 1) {
1062            $email = empty($tmp = $client->getAttribute('bill_email_address')->value->getValue())
1063                ? (string) $tmp
1064                : $client->account->getEmail();
1065
1066            $this->sendBillEmail($media, $email, $response->header->l11n->language);
1067        }
1068
1069        // Add type to media
1070        /** @var \Model\Setting $originalType */
1071        $originalType = $this->app->appSettings->get(
1072            names: SettingsEnum::ORIGINAL_MEDIA_TYPE,
1073            module: self::NAME
1074        );
1075
1076        $this->createModelRelation(
1077            $request->header->account,
1078            $media->id,
1079            (int) $originalType->content,
1080            MediaMapper::class,
1081            'types',
1082            '',
1083            $request->getOrigin()
1084        );
1085
1086        // Add media to bill
1087        $this->createModelRelation(
1088            $request->header->account,
1089            $bill->id,
1090            $media->id,
1091            BillMapper::class,
1092            'files',
1093            '',
1094            $request->getOrigin()
1095        );
1096
1097        $this->createStandardCreateResponse($request, $response, $media);
1098    }
1099
1100    /**
1101     * Send bill as email
1102     *
1103     * @param Media  $media    Media to send
1104     * @param string $email    Email address
1105     * @param string $language Message language
1106     *
1107     * @return void
1108     *
1109     * @since 1.0.0
1110     */
1111    public function sendBillEmail(Media $media, string $email, string $language = 'en') : void
1112    {
1113        $handler = $this->app->moduleManager->get('Admin', 'Api')->setUpServerMailHandler();
1114
1115        /** @var \Model\Setting $emailFrom */
1116        $emailFrom = $this->app->appSettings->get(
1117            names: AdminSettingsEnum::MAIL_SERVER_ADDR,
1118            module: 'Admin'
1119        );
1120
1121        /** @var \Model\Setting $billingTemplate */
1122        $billingTemplate = $this->app->appSettings->get(
1123            names: SettingsEnum::BILLING_CUSTOMER_EMAIL_TEMPLATE,
1124            module: 'Billing'
1125        );
1126
1127        $mail = EmailMapper::get()
1128            ->with('l11n')
1129            ->where('id', (int) $billingTemplate->content)
1130            ->where('l11n/language', $language)
1131            ->execute();
1132
1133        $mail = new Email();
1134        $mail->setFrom($emailFrom->content);
1135        $mail->addTo($email);
1136        $mail->addAttachment($media->getAbsolutePath(), $media->name);
1137
1138        $handler->send($mail);
1139
1140        $this->app->moduleManager->get('Billing', 'Api')->sendMail($mail);
1141    }
1142
1143    /**
1144     * Api method to create bill files
1145     *
1146     * @param RequestAbstract  $request  Request
1147     * @param ResponseAbstract $response Response
1148     * @param array            $data     Generic data
1149     *
1150     * @return void
1151     *
1152     * @api
1153     *
1154     * @since 1.0.0
1155     */
1156    public function apiNoteCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1157    {
1158        if (!empty($val = $this->validateNoteCreate($request))) {
1159            $response->header->status = RequestStatusCode::R_400;
1160            $this->createInvalidCreateResponse($request, $response, $val);
1161
1162            return;
1163        }
1164
1165        /** @var \Modules\Billing\Models\Bill $bill */
1166        $bill = BillMapper::get()->where('id', (int) $request->getData('id'))->execute();
1167
1168        $request->setData('virtualpath', $this->createBillDir($bill), true);
1169        $this->app->moduleManager->get('Editor')->apiEditorCreate($request, $response, $data);
1170
1171        if ($response->header->status !== RequestStatusCode::R_200) {
1172            return;
1173        }
1174
1175        /** @var \Modules\Editor\Models\EditorDoc $model */
1176        $model = $response->getDataArray($request->uri->__toString())['response'];
1177        $this->createModelRelation($request->header->account, $request->getDataInt('id'), $model->id, BillMapper::class, 'bill_note', '', $request->getOrigin());
1178    }
1179
1180    /**
1181     * Validate bill note create request
1182     *
1183     * @param RequestAbstract $request Request
1184     *
1185     * @return array<string, bool>
1186     *
1187     * @since 1.0.0
1188     */
1189    private function validateNoteCreate(RequestAbstract $request) : array
1190    {
1191        $val = [];
1192        if (($val['id'] = !$request->hasData('id'))) {
1193            return $val;
1194        }
1195
1196        return [];
1197    }
1198
1199    /**
1200     * Api method to delete Bill
1201     *
1202     * @param RequestAbstract  $request  Request
1203     * @param ResponseAbstract $response Response
1204     * @param array            $data     Generic data
1205     *
1206     * @return void
1207     *
1208     * @api
1209     *
1210     * @since 1.0.0
1211     */
1212    public function apiBillDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1213    {
1214        if (!empty($val = $this->validateBillDelete($request))) {
1215            $response->header->status = RequestStatusCode::R_400;
1216            $this->createInvalidDeleteResponse($request, $response, $val);
1217
1218            return;
1219        }
1220
1221        /** @var \Modules\Billing\Models\Bill $old */
1222        $old = BillMapper::get()->where('id', (int) $request->getData('id'))->execute();
1223
1224        // @todo: check if bill can be deleted
1225        // @todo: adjust stock transfer
1226
1227        $new = $this->deleteBillFromRequest($request, clone $old);
1228        $this->updateModel($request->header->account, $old, $new, BillMapper::class, 'bill', $request->getOrigin());
1229        $this->createStandardDeleteResponse($request, $response, $old);
1230    }
1231
1232    /**
1233     * Method to create a bill from request.
1234     *
1235     * @param RequestAbstract $request Request
1236     * @param Bill            $new     Bill
1237     *
1238     * @return Bill
1239     *
1240     * @since 1.0.0
1241     */
1242    public function deleteBillFromRequest(RequestAbstract $request, Bill $new) : Bill
1243    {
1244        $new->status = BillStatus::DELETED;
1245
1246        return $new;
1247    }
1248
1249    /**
1250     * Validate Bill delete request
1251     *
1252     * @param RequestAbstract $request Request
1253     *
1254     * @return array<string, bool>
1255     *
1256     * @todo: implement
1257     *
1258     * @since 1.0.0
1259     */
1260    private function validateBillDelete(RequestAbstract $request) : array
1261    {
1262        $val = [];
1263        if (($val['id'] = !$request->hasData('id'))) {
1264            return $val;
1265        }
1266
1267        return [];
1268    }
1269
1270    /**
1271     * Api method to update BillElement
1272     *
1273     * @param RequestAbstract  $request  Request
1274     * @param ResponseAbstract $response Response
1275     * @param array            $data     Generic data
1276     *
1277     * @return void
1278     *
1279     * @api
1280     *
1281     * @since 1.0.0
1282     */
1283    public function apiBillElementUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1284    {
1285        if (!empty($val = $this->validateBillElementUpdate($request))) {
1286            $response->header->status = RequestStatusCode::R_400;
1287            $this->createInvalidUpdateResponse($request, $response, $val);
1288
1289            return;
1290        }
1291
1292        /** @var BillElement $old */
1293        $old = BillElementMapper::get()->where('id', (int) $request->getData('id'))->execute();
1294
1295        // @todo: can be edited?
1296        // @todo: adjust transfer protocolls
1297
1298        $new = $this->updateBillElementFromRequest($request, clone $old);
1299
1300        $this->updateModel($request->header->account, $old, $new, BillElementMapper::class, 'bill_element', $request->getOrigin());
1301        $this->createStandardUpdateResponse($request, $response, $new);
1302    }
1303
1304    /**
1305     * Method to update BillElement from request.
1306     *
1307     * @param RequestAbstract $request Request
1308     * @param BillElement     $new     Model to modify
1309     *
1310     * @return BillElement
1311     *
1312     * @todo: implement
1313     *
1314     * @since 1.0.0
1315     */
1316    public function updateBillElementFromRequest(RequestAbstract $request, BillElement $new) : BillElement
1317    {
1318        return $new;
1319    }
1320
1321    /**
1322     * Validate BillElement update request
1323     *
1324     * @param RequestAbstract $request Request
1325     *
1326     * @return array<string, bool>
1327     *
1328     * @todo: implement
1329     *
1330     * @since 1.0.0
1331     */
1332    private function validateBillElementUpdate(RequestAbstract $request) : array
1333    {
1334        $val = [];
1335        if (($val['id'] = !$request->hasData('id'))) {
1336            return $val;
1337        }
1338
1339        return [];
1340    }
1341
1342    /**
1343     * Api method to delete BillElement
1344     *
1345     * @param RequestAbstract  $request  Request
1346     * @param ResponseAbstract $response Response
1347     * @param array            $data     Generic data
1348     *
1349     * @return void
1350     *
1351     * @api
1352     *
1353     * @since 1.0.0
1354     */
1355    public function apiBillElementDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1356    {
1357        if (!empty($val = $this->validateBillElementDelete($request))) {
1358            $response->header->status = RequestStatusCode::R_400;
1359            $this->createInvalidDeleteResponse($request, $response, $val);
1360
1361            return;
1362        }
1363
1364        // @todo: check if can be deleted
1365        // @todo: handle transactions and bill update
1366
1367        /** @var \Modules\Billing\Models\BillElement $billElement */
1368        $billElement = BillElementMapper::get()->where('id', (int) $request->getData('id'))->execute();
1369        $this->deleteModel($request->header->account, $billElement, BillElementMapper::class, 'bill_element', $request->getOrigin());
1370        $this->createStandardDeleteResponse($request, $response, $billElement);
1371    }
1372
1373    /**
1374     * Validate BillElement delete request
1375     *
1376     * @param RequestAbstract $request Request
1377     *
1378     * @return array<string, bool>
1379     *
1380     * @since 1.0.0
1381     */
1382    private function validateBillElementDelete(RequestAbstract $request) : array
1383    {
1384        $val = [];
1385        if (($val['id'] = !$request->hasData('id'))) {
1386            return $val;
1387        }
1388
1389        return [];
1390    }
1391
1392    /**
1393     * Api method to update Note
1394     *
1395     * @param RequestAbstract  $request  Request
1396     * @param ResponseAbstract $response Response
1397     * @param array            $data     Generic data
1398     *
1399     * @return void
1400     *
1401     * @api
1402     *
1403     * @since 1.0.0
1404     */
1405    public function apiNoteUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1406    {
1407        // @todo: check permissions
1408        $this->app->moduleManager->get('Editor', 'Api')->apiEditorDocUpdate($request, $response, $data);
1409    }
1410
1411    /**
1412     * Api method to delete Note
1413     *
1414     * @param RequestAbstract  $request  Request
1415     * @param ResponseAbstract $response Response
1416     * @param array            $data     Generic data
1417     *
1418     * @return void
1419     *
1420     * @api
1421     *
1422     * @since 1.0.0
1423     */
1424    public function apiNoteDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void
1425    {
1426        // @todo: check permissions
1427        $this->app->moduleManager->get('Editor', 'Api')->apiEditorDocDelete($request, $response, $data);
1428    }
1429}