From 5bd411b3949231dd07620bd0e74f9a4906ee6e00 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 8 Aug 2024 12:32:31 +1000 Subject: [PATCH 1/7] Working on Peppol --- app/Mail/Engine/PaymentEmailEngine.php | 1 + .../EDocument/Gateway/Storecove/Storecove.php | 6 +- app/Services/EDocument/Standards/Peppol.php | 231 ++++++++++++++++-- composer.json | 3 +- composer.lock | 19 +- lang/en/texts.php | 2 +- .../Einvoice/Storecove/StorecoveTest.php | 22 +- 7 files changed, 243 insertions(+), 41 deletions(-) diff --git a/app/Mail/Engine/PaymentEmailEngine.php b/app/Mail/Engine/PaymentEmailEngine.php index d23772402310..c5832db31fdb 100644 --- a/app/Mail/Engine/PaymentEmailEngine.php +++ b/app/Mail/Engine/PaymentEmailEngine.php @@ -262,6 +262,7 @@ class PaymentEmailEngine extends BaseEmailEngine $data['$client.email'] = &$data['$email']; $data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; + $data['$client.payment_balance'] = ['value' => Number::formatMoney($this->client->payment_balance, $this->client), 'label' => ctrans('texts.payment_balance_on_file')]; $data['$outstanding'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')]; diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index 397e8813ff2b..ede48f486e8e 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -137,14 +137,14 @@ class Storecove { } - public function sendDocument(string $document, int $routing_id, array $identifiers = []) + public function sendDocument(string $document, int $routing_id, array $override_payload = []) { $payload = [ "legalEntityId" => $routing_id, "idempotencyGuid"=> \Illuminate\Support\Str::uuid(), "routing" => [ - "eIdentifiers" => $identifiers, + "eIdentifiers" => [], "emails" => ["david@invoiceninja.com"] ], "document"=> [ @@ -157,6 +157,8 @@ class Storecove { ], ]; + $payload = array_merge($payload, $override_payload); + $uri = "document_submissions"; nlog($payload); diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index d6c6e8f538ee..4e58949853b9 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -13,22 +13,25 @@ namespace App\Services\EDocument\Standards; use App\Models\Company; use App\Models\Invoice; +use App\Helpers\Invoice\Taxer; use App\Services\AbstractService; use App\Helpers\Invoice\InvoiceSum; use InvoiceNinja\EInvoice\EInvoice; +use App\Utils\Traits\NumberFormatter; use App\Helpers\Invoice\InvoiceSumInclusive; -use App\Helpers\Invoice\Taxer; use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans; use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item; use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party; use InvoiceNinja\EInvoice\Models\Peppol\PriceType\Price; +use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address; use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact; use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country; +use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount; +use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal; use App\Services\EDocument\Standards\Settings\PropertyResolver; -use App\Utils\Traits\NumberFormatter; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PriceAmount; use InvoiceNinja\EInvoice\Models\Peppol\PartyNameType\PartyName; use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme; @@ -42,14 +45,13 @@ use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount; +use InvoiceNinja\EInvoice\Models\Peppol\OrderReferenceType\OrderReference; use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal; use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory; use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty; use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; -use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; -use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; -use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; +use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID; class Peppol extends AbstractService { @@ -103,7 +105,7 @@ class Peppol extends AbstractService 'DE' => 'VAT', //tested - requires Payment Means to be defined. 'DK' => 'ERST', 'EE' => 'VAT', - 'ES' => 'VAT', + 'ES' => 'VAT', //tested - B2G pending 'FI' => 'VAT', 'FR' => 'VAT', 'GR' => 'VAT', @@ -165,6 +167,8 @@ class Peppol extends AbstractService private EInvoice $e; + private array $storecove_meta = []; + /** * @param Invoice $invoice */ @@ -649,7 +653,7 @@ class Peppol extends AbstractService { $acp = new AccountingCustomerParty(); - + $party = new Party(); if(strlen($this->invoice->client->vat_number ?? '') > 1) { @@ -740,7 +744,15 @@ class Peppol extends AbstractService return $total; } + + ///////////////// Helper Methods ///////////////////////// + /** + * setInvoiceDefaults + * + * Stubs a default einvoice + * @return self + */ public function setInvoiceDefaults(): self { $settings = [ @@ -769,7 +781,15 @@ class Peppol extends AbstractService return $this; } - + + /** + * getSetting + * + * Attempts to harvest and return a preconfigured prop from company / client / invoice settings + * + * @param string $property_path + * @return mixed + */ public function getSetting(string $property_path): mixed { @@ -783,8 +803,14 @@ class Peppol extends AbstractService return null; } - - public function countryLevelMutators():self + + /** + * countryLevelMutators + * + * Runs country level specific requirements for the e-invoice + * @return self + */ + private function countryLevelMutators():self { if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) @@ -792,7 +818,14 @@ class Peppol extends AbstractService return $this; } - + + /** + * setPaymentMeans + * + * Sets the payment means - if it exists + * @param bool $required + * @return self + */ private function setPaymentMeans(bool $required = false): self { @@ -803,12 +836,132 @@ class Peppol extends AbstractService return $this; } - if($required) - throw new \Exception('e-invoice generation halted:: Payment Means required'); + return $this->checkRequired($required, "Payment Means"); + + } + + /** + * setOrderReference + * + * sets the order reference - if it exists (Never rely on settings for this) + * + * @param bool $required + * @return self + */ + private function setOrderReference(bool $required = false): self + { + $this->p_invoice->BuyerReference = $this->invoice->po_number ?? ''; + + if(strlen($this->invoice->po_number ?? '') > 1) + { + $order_reference = new OrderReference(); + $id = new ID(); + $id->value = $this->invoice->po_number; + + $order_reference->ID = $id; + + $this->p_invoice->OrderReference = $order_reference; + + $this->setStorecoveMeta(["document" => [ + "invoice" => [ + "references" => [ + "documentType" => "purchase_order", + "documentId" => $this->invoice->po_number, + ], + ], + ] + ]); + + return $this; + } + + return $this->checkRequired($required, 'Order Reference'); - return $this; } + /** + * setCustomerAssignedAccountId + * + * Sets the client id_number CAN rely on settings + * + * @param bool $required + * @return self + */ + private function setCustomerAssignedAccountId(bool $required = false): self + { + //@phpstan-ignore-next-line + if(isset($this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID)){ + return $this; + } + elseif($customer_assigned_account_id = $this->getSetting('Invoice.AccountingCustomerParty.CustomerAssignedAccountID')){ + + $this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id; + return $this; + } + elseif(strlen($this->invoice->client->id_number ?? '') > 1){ + + $customer_assigned_account_id = new CustomerAssignedAccountID(); + $customer_assigned_account_id->value = $this->invoice->client->id_number; + + $this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id; + return $this; + } + + //@phpstan-ignore-next-line + return $this->checkRequired($required, 'Client ID Number'); + + } + + /** + * Check Required + * + * Throws if a required field is missing. + * + * @param bool $required + * @param string $section + * @return self + */ + private function checkRequired(bool $required, string $section): self + { + + return $required ? throw new \Exception("e-invoice generation halted:: {$section} required") : $this; + + } + + + /** + * Builds the Routing object for StoreCove + * + * @param string $schemeId + * @param string $id + * @return array + */ + private function buildRouting(string $schemeId, string $id): array + { + + return + [ + "routing" => [ + "publicIdentifiers" => [ + [ + "scheme" => $schemeId, + "id" => $id + ] + ] + ] + ]; + } + + + + + + + + + + ////////////////////////// Country level mutators ///////////////////////////////////// + /** * DE * @@ -824,7 +977,21 @@ class Peppol extends AbstractService return $this; } - + + /** + * setStorecoveMeta + * + * updates the storecove payload for sending documents + * + * @param array $meta + * @return self + */ + private function setStorecoveMeta(array $meta): self + { + $this->storecove_meta = array_merge($this->storecove_meta, $meta); + + return $this; + } /** * CH * @@ -867,10 +1034,10 @@ class Peppol extends AbstractService * ES * * @Pending + * B2G configuration + * B2G Testing * - * ES:DIRE - routing identifier - * - * testing. //293098 + * testing. // routing identifier - 293098 * * @return self */ @@ -923,17 +1090,37 @@ class Peppol extends AbstractService private function FR(): self { + // When sending invoices to the French government (Chorus Pro): - // All invoices have to be routed to SIRET 0009:11000201100044. There is no test environment for sending to public entities. - // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + if($this->invoice->client->classification == 'government'){ + //route to SIRET 0009:11000201100044 + $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:11000201100044")); + + // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + $this->setCustomerAssignedAccountId(true); + + } + + + if(strlen($this->invoice->client->id_number ?? '') == 9) { + //SIREN + $this->setStorecoveMeta($this->buildRouting('FR:SIREN', "0002:{$this->invoice->client->id_number}")); + } + else { + //SIRET + $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:{$this->invoice->client->id_number}")); + } + + // ??????????????????????? //@TODO // The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference) - // The commitment number must be sent in the invoice.orderReference (deprecated) or the invoice.references array (documentType purchase_order). + if(strlen($this->invoice->po_number ?? '') >1) { + $this->setOrderReference(false); + } - // Invoices to companies (SIRET / 0009 or SIRENE / 0002) are routed directly to that identifier. return $this; } diff --git a/composer.json b/composer.json index f33899c9ee3c..cba4ad290b11 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "asm/php-ansible": "dev-main", "authorizenet/authorizenet": "^2.0", "awobaz/compoships": "^2.1", + "aws/aws-sdk-php": "^3.319", "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "dev-master", "braintree/braintree_php": "^6.0", @@ -201,4 +202,4 @@ ], "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 55d090faa651..4fb0ae985936 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6eda3a2962158b87dab46711e65a8438", + "content-hash": "95e7bd229644d1d8e768ecfbc78582cd", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -535,16 +535,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.317.1", + "version": "3.319.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9" + "reference": "a5c408d4cd1945d5fc817f45e46383634b610497" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dc1e3031c2721a25beb2e8fbb175b576e3d60ab9", - "reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a5c408d4cd1945d5fc817f45e46383634b610497", + "reference": "a5c408d4cd1945d5fc817f45e46383634b610497", "shasum": "" }, "require": { @@ -597,7 +597,10 @@ ], "psr-4": { "Aws\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -624,9 +627,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.317.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.319.0" }, - "time": "2024-08-02T18:09:42+00:00" + "time": "2024-08-07T18:05:51+00:00" }, { "name": "bacon/bacon-qr-code", diff --git a/lang/en/texts.php b/lang/en/texts.php index 6a1dcd6af06a..37bbb24a3f46 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5313,7 +5313,7 @@ $lang = array( 'forever_free' => 'Forever Free', 'comments_only' => 'Comments Only', 'payment_balance_on_file' => 'Payment Balance On File', - + 'ubl_email_attachment_help' => 'For more e-invoice settings please navigate :here', ); return $lang; diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index d227c6848254..861f428994d3 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -639,10 +639,14 @@ $x = ' nlog($xml); $identifiers = [ - [ - 'scheme' => 'ES:VAT', - 'id' => 'ESB53625999' - ], + "routing" => [ + "eIdentifiers" => [ + [ + 'scheme' => 'ES:VAT', + 'id' => 'ESB53625999' + ], + ] + ] ]; $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); @@ -673,9 +677,13 @@ $x = ' nlog($xml); $identifiers = [ - [ - 'scheme' => 'DE:VAT', - 'id' => 'DE010101010' + "routing" => [ + "eIdentifiers" => [ + [ + 'scheme' => 'DE:VAT', + 'id' => 'DE010101010' + ] + ] ] ]; From a8e313b5e9f799ba582b50d8c7c32db76ea58422 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 8 Aug 2024 14:16:35 +1000 Subject: [PATCH 2/7] Working on Peppol --- app/PaymentDrivers/BaseDriver.php | 2 + app/Services/EDocument/Standards/Peppol.php | 8 +- .../Einvoice/Storecove/StorecoveTest.php | 114 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 4c7dd2691013..26489ff7df13 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -400,6 +400,7 @@ class BaseDriver extends AbstractPaymentDriver return; $invoices = Invoice::query() + ->where('company_id', $this->company_gateway->company_id) ->whereIn('id', $this->transformKeys(array_column($payment_invoices, 'invoice_id'))) ->whereJsonContains('line_items', ['type_id' => '3']) ->withTrashed(); @@ -407,6 +408,7 @@ class BaseDriver extends AbstractPaymentDriver if($invoices->count() == 0){ $invoice = Invoice::query() + ->where('company_id', $this->company_gateway->company_id) ->whereIn('id', $this->transformKeys(array_column($payment_invoices, 'invoice_id'))) ->orderBy('id','desc') ->withTrashed() diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 4e58949853b9..c01d6f7c23ce 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -1087,7 +1087,13 @@ class Peppol extends AbstractService return $this; } - + + /** + * FR + * @Pending - clarification on codes needed + * + * @return self + */ private function FR(): self { diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index 861f428994d3..fa7408d52291 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -503,6 +503,120 @@ $x = ' return $invoice; + } + + private function createFRData() + { + $this->routing_id = 293338; + + +$settings = CompanySettings::defaults(); +$settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; +$settings->website = 'www.invoiceninja.de'; + +$settings->address1 = '10 Rue de la Paix'; +$settings->address2 = 'Bâtiment A, Bureau 5'; +$settings->city = 'Paris'; +$settings->state = 'Île-de-France'; +$settings->postal_code = '75002'; +$settings->phone = '01 23456789'; +$settings->email = $this->faker->unique()->safeEmail(); +$settings->country_id = '250'; // France's ISO country code +$settings->vat_number = 'FR12345678901'; +$settings->id_number = '12345678900010'; + +$settings->use_credits_payment = 'always'; +$settings->timezone_id = '1'; // CET (Central European Time) +$settings->entity_send_time = 0; +$settings->e_invoice_type = 'PEPPOL'; +$settings->currency_id = '3'; + +$company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, +]); + +$this->user->companies()->attach($company->id, [ + 'account_id' => $this->account->id, + 'is_owner' => true, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, +]); + +Client::unguard(); + +$c = +Client::create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Beispiel Firma GmbH', + 'website' => 'https://www.beispiel-firma.de', + 'private_notes' => 'Dies sind private Notizen zum Testkunden.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'DE654321987', + 'id_number' => 'HRB 12345', // Typical format for German company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'blau', + 'custom_value3' => 'musterwort', + 'custom_value4' => 'test@example.com', + 'address1' => 'Musterstraße 123', + 'address2' => '2. Etage, Büro 45', + 'city' => 'München', + 'state' => 'Bayern', + 'postal_code' => '80331', + 'country_id' => '276', // Germany + 'shipping_address1' => 'Musterstraße 123', + 'shipping_address2' => '2. Etage, Büro 45', + 'shipping_city' => 'München', + 'shipping_state' => 'Bayern', + 'shipping_postal_code' => '80331', + 'shipping_country_id' => '276', // Germany + 'settings' => ClientSettings::Defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => '', +]); + + +$item = new InvoiceItem(); +$item->product_key = "Product Key"; +$item->notes = "Product Description"; +$item->cost = 10; +$item->quantity = 10; +$item->tax_rate1 = 19; +$item->tax_name1 = 'mwst'; + +$invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'discount' => 0, + 'uses_inclusive_taxes' => false, + 'status_id' => 1, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name2' => '', + 'tax_name3' => '', + 'line_items' => [$item], + 'number' => 'DE-'.rand(1000, 100000), + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(14)->format('Y-m-d'), +]); + +$invoice = $invoice->calc()->getInvoice(); +$invoice->service()->markSent()->save(); + + +return $invoice; + + + + } private function createDEData() From 286b2087cd01b6711ee82e6d74b49b1921d62a00 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 8 Aug 2024 17:48:24 +1000 Subject: [PATCH 3/7] Updated URL for trouble shooting --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- app/Models/Invoice.php | 8 +- .../EDocument/Gateway/Storecove/Storecove.php | 15 +- app/Services/EDocument/Standards/Peppol.php | 55 +++-- .../Einvoice/Storecove/StorecoveTest.php | 225 ++++++++++-------- 5 files changed, 171 insertions(+), 134 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index badbdef1d55d..6feefd261a06 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,7 +8,7 @@ assignees: '' --- +https://invoiceninja.github.io/en/self-host-troubleshooting/ --> ## Setup - Version: diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 2383c0f42af0..fe2b1222f391 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -614,17 +614,17 @@ class Invoice extends BaseModel event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); break; case 'reminder1': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'reminder2': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'reminder3': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'reminder_endless': case 'endless_reminder': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template)); break; case 'custom1': case 'custom2': diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index ede48f486e8e..9734abfa587e 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -148,17 +148,20 @@ class Storecove { "emails" => ["david@invoiceninja.com"] ], "document"=> [ - 'documentType' => 'invoice', - "rawDocumentData"=> [ - "document" => base64_encode($document), - "parse" => true, - "parseStrategy"=> "ubl", - ], + ], ]; $payload = array_merge($payload, $override_payload); + + $payload['document']['documentType'] = 'invoice'; + $payload['document']["rawDocumentData"] = [ + "document" => base64_encode($document), + "parse" => true, + "parseStrategy"=> "ubl", + ]; + $uri = "document_submissions"; nlog($payload); diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index c01d6f7c23ce..8540e214802e 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -862,15 +862,17 @@ class Peppol extends AbstractService $this->p_invoice->OrderReference = $order_reference; - $this->setStorecoveMeta(["document" => [ - "invoice" => [ - "references" => [ - "documentType" => "purchase_order", - "documentId" => $this->invoice->po_number, - ], - ], - ] - ]); + // $this->setStorecoveMeta(["document" => [ + // "invoice" => [ + // [ + // "references" => [ + // "documentType" => "purchase_order", + // "documentId" => $this->invoice->po_number, + // ], + // ], + // ], + // ] + // ]); return $this; } @@ -952,7 +954,25 @@ class Peppol extends AbstractService ]; } + /** + * setStorecoveMeta + * + * updates the storecove payload for sending documents + * + * @param array $meta + * @return self + */ + private function setStorecoveMeta(array $meta): self + { + $this->storecove_meta = array_merge($this->storecove_meta, $meta); + + return $this; + } + public function getStorecoveMeta(): array + { + return $this->storecove_meta; + } @@ -977,21 +997,7 @@ class Peppol extends AbstractService return $this; } - - /** - * setStorecoveMeta - * - * updates the storecove payload for sending documents - * - * @param array $meta - * @return self - */ - private function setStorecoveMeta(array $meta): self - { - $this->storecove_meta = array_merge($this->storecove_meta, $meta); - - return $this; - } + /** * CH * @@ -1109,7 +1115,6 @@ class Peppol extends AbstractService $this->setCustomerAssignedAccountId(true); } - if(strlen($this->invoice->client->id_number ?? '') == 9) { //SIREN diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index fa7408d52291..e6e619dd8e39 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -509,113 +509,110 @@ $x = ' { $this->routing_id = 293338; + $settings = CompanySettings::defaults(); + $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; + $settings->website = 'www.invoiceninja.de'; -$settings = CompanySettings::defaults(); -$settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; -$settings->website = 'www.invoiceninja.de'; + $settings->address1 = '10 Rue de la Paix'; + $settings->address2 = 'Bâtiment A, Bureau 5'; + $settings->city = 'Paris'; + $settings->state = 'Île-de-France'; + $settings->postal_code = '75002'; + $settings->phone = '01 23456789'; + $settings->email = $this->faker->unique()->safeEmail(); + $settings->country_id = '250'; // France's ISO country code + $settings->vat_number = 'FR82345678911'; + $settings->id_number = '12345678900010'; + $settings->classification = 'business'; + $settings->use_credits_payment = 'always'; + $settings->timezone_id = '1'; // CET (Central European Time) + $settings->entity_send_time = 0; + $settings->e_invoice_type = 'PEPPOL'; + $settings->currency_id = '3'; -$settings->address1 = '10 Rue de la Paix'; -$settings->address2 = 'Bâtiment A, Bureau 5'; -$settings->city = 'Paris'; -$settings->state = 'Île-de-France'; -$settings->postal_code = '75002'; -$settings->phone = '01 23456789'; -$settings->email = $this->faker->unique()->safeEmail(); -$settings->country_id = '250'; // France's ISO country code -$settings->vat_number = 'FR12345678901'; -$settings->id_number = '12345678900010'; + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); -$settings->use_credits_payment = 'always'; -$settings->timezone_id = '1'; // CET (Central European Time) -$settings->entity_send_time = 0; -$settings->e_invoice_type = 'PEPPOL'; -$settings->currency_id = '3'; + $this->user->companies()->attach($company->id, [ + 'account_id' => $this->account->id, + 'is_owner' => true, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); -$company = Company::factory()->create([ - 'account_id' => $this->account->id, - 'settings' => $settings, -]); + Client::unguard(); -$this->user->companies()->attach($company->id, [ - 'account_id' => $this->account->id, - 'is_owner' => true, - 'is_admin' => 1, - 'is_locked' => 0, - 'permissions' => '', - 'notifications' => CompanySettings::notificationAdminDefaults(), - 'settings' => null, -]); - -Client::unguard(); - -$c = -Client::create([ - 'company_id' => $company->id, - 'user_id' => $this->user->id, - 'name' => 'Beispiel Firma GmbH', - 'website' => 'https://www.beispiel-firma.de', - 'private_notes' => 'Dies sind private Notizen zum Testkunden.', - 'balance' => 0, - 'paid_to_date' => 0, - 'vat_number' => 'DE654321987', - 'id_number' => 'HRB 12345', // Typical format for German company registration numbers - 'custom_value1' => '2024-07-22 10:00:00', - 'custom_value2' => 'blau', - 'custom_value3' => 'musterwort', - 'custom_value4' => 'test@example.com', - 'address1' => 'Musterstraße 123', - 'address2' => '2. Etage, Büro 45', - 'city' => 'München', - 'state' => 'Bayern', - 'postal_code' => '80331', - 'country_id' => '276', // Germany - 'shipping_address1' => 'Musterstraße 123', - 'shipping_address2' => '2. Etage, Büro 45', - 'shipping_city' => 'München', - 'shipping_state' => 'Bayern', - 'shipping_postal_code' => '80331', - 'shipping_country_id' => '276', // Germany - 'settings' => ClientSettings::Defaults(), - 'client_hash' => \Illuminate\Support\Str::random(32), - 'routing_id' => '', -]); + $c = + Client::create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Exemple Société S.A.', + 'website' => 'https://www.exemple-societe.fr', + 'private_notes' => 'Ceci est une note privée pour le client test.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'FR12345678901', + 'id_number' => '12345678900010', // Typical format for French company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'bleu', + 'custom_value3' => 'motexemple', + 'custom_value4' => 'test@example.com', + 'address1' => '123 Rue de l\'Exemple', + 'address2' => '2ème étage, Bureau 45', + 'city' => 'Paris', + 'state' => 'Île-de-France', + 'postal_code' => '75001', + 'country_id' => '250', // France + 'shipping_address1' => '123 Rue de l\'Exemple', + 'shipping_address2' => '2ème étage, Bureau 45', + 'shipping_city' => 'Paris', + 'shipping_state' => 'Île-de-France', + 'shipping_postal_code' => '75001', + 'shipping_country_id' => '250', // France + 'classification' => 'business', + 'settings' => ClientSettings::Defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => '', + ]); -$item = new InvoiceItem(); -$item->product_key = "Product Key"; -$item->notes = "Product Description"; -$item->cost = 10; -$item->quantity = 10; -$item->tax_rate1 = 19; -$item->tax_name1 = 'mwst'; + $item = new InvoiceItem(); + $item->product_key = "Product Key"; + $item->notes = "Product Description"; + $item->cost = 10; + $item->quantity = 10; + $item->tax_rate1 = 20; + $item->tax_name1 = 'VAT'; -$invoice = Invoice::factory()->create([ - 'company_id' => $company->id, - 'user_id' => $this->user->id, - 'client_id' => $c->id, - 'discount' => 0, - 'uses_inclusive_taxes' => false, - 'status_id' => 1, - 'tax_rate1' => 0, - 'tax_name1' => '', - 'tax_rate2' => 0, - 'tax_rate3' => 0, - 'tax_name2' => '', - 'tax_name3' => '', - 'line_items' => [$item], - 'number' => 'DE-'.rand(1000, 100000), - 'date' => now()->format('Y-m-d'), - 'due_date' => now()->addDays(14)->format('Y-m-d'), -]); + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'discount' => 0, + 'uses_inclusive_taxes' => false, + 'status_id' => 1, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name2' => '', + 'tax_name3' => '', + 'line_items' => [$item], + 'number' => 'DE-'.rand(1000, 100000), + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(14)->format('Y-m-d'), + ]); -$invoice = $invoice->calc()->getInvoice(); -$invoice->service()->markSent()->save(); - - -return $invoice; - + $invoice = $invoice->calc()->getInvoice(); + $invoice->service()->markSent()->save(); + return $invoice; } @@ -728,7 +725,39 @@ return $invoice; } - public function testEsRules() + public function testFrRules() + { + + $invoice = $this->createFRData(); + + $e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + + $stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}'); + foreach($stub as $key => $value) { + $e_invoice->{$key} = $value; + } + + $invoice->e_invoice = $e_invoice; + $invoice->save(); + + $this->assertInstanceOf(Invoice::class, $invoice); + $this->assertInstanceof(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $e_invoice); + + $p = new Peppol($invoice); + + $p->run(); + $xml = $p->toXml(); + nlog($xml); + + $identifiers = $p->getStorecoveMeta(); + + $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); + $sc->sendDocument($xml, $this->routing_id, $identifiers); + + } + + + public function RtestEsRules() { $invoice = $this->createESData(); @@ -768,7 +797,7 @@ return $invoice; } - public function testDeRules() + public function RtestDeRules() { $invoice = $this->createDEData(); From ec144b41819e12a3163ffb04924b3055054fae32 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 9 Aug 2024 15:39:16 +1000 Subject: [PATCH 4/7] Fixes for Rotessa --- app/PaymentDrivers/Rotessa/PaymentMethod.php | 13 +++++++++++-- app/PaymentDrivers/RotessaPaymentDriver.php | 13 +++++++++---- .../ninja2020/payment_methods/index.blade.php | 11 +++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/PaymentDrivers/Rotessa/PaymentMethod.php b/app/PaymentDrivers/Rotessa/PaymentMethod.php index 3e617f76f963..4883490319e7 100755 --- a/app/PaymentDrivers/Rotessa/PaymentMethod.php +++ b/app/PaymentDrivers/Rotessa/PaymentMethod.php @@ -100,8 +100,17 @@ class PaymentMethod implements MethodInterface $customer = array_merge(['address' => $request->only('address_1','address_2','city','postal_code','province_code','country'), 'custom_identifier' => $request->input('custom_identifier') ], $request->all()); - $this->rotessa->findOrCreateCustomer($customer); - + try{ + $this->rotessa->findOrCreateCustomer($customer); + } + catch(\Exception $e){ + + $message = json_decode($e->getMessage(), true); + + return redirect()->route('client.payment_methods.index')->withErrors(array_values($message['errors'])); + + } + return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added')); } diff --git a/app/PaymentDrivers/RotessaPaymentDriver.php b/app/PaymentDrivers/RotessaPaymentDriver.php index 040a8c65e935..e52b206e9823 100644 --- a/app/PaymentDrivers/RotessaPaymentDriver.php +++ b/app/PaymentDrivers/RotessaPaymentDriver.php @@ -202,7 +202,6 @@ class RotessaPaymentDriver extends BaseDriver public function findOrCreateCustomer(array $data) { - nlog($data); $result = null; try { @@ -219,7 +218,6 @@ class RotessaPaymentDriver extends BaseDriver if(!isset($data['id'])) { - nlog("no id, lets goo"); $result = $this->gatewayRequest('post', 'customers', $data); if($result->failed()) @@ -252,9 +250,16 @@ class RotessaPaymentDriver extends BaseDriver 'code' => 500 ]; - SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company); + SystemLogger::dispatch(['server_response' => $data, 'data' => []], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company); - throw $th; + try{ + $errors = explode("422:", $th->getMessage())[1]; + } + catch(\Exception){ + $errors = 'Unknown error occured'; + } + + throw new \Exception($errors, $th->getCode()); } } diff --git a/resources/views/portal/ninja2020/payment_methods/index.blade.php b/resources/views/portal/ninja2020/payment_methods/index.blade.php index 8bdde30840f7..9e73a95b3e36 100644 --- a/resources/views/portal/ninja2020/payment_methods/index.blade.php +++ b/resources/views/portal/ninja2020/payment_methods/index.blade.php @@ -2,6 +2,17 @@ @section('meta_title', ctrans('texts.payment_methods')) @section('body') + + @section('header') + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + @endsection +
@livewire('payment-methods-table', ['client_id' => $client->id, 'db' => $company->db])
From 489476feeb87ef32e2c26bf71377af0f59ff870c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 10 Aug 2024 08:25:32 +1000 Subject: [PATCH 5/7] Fixes for quotas --- app/Jobs/Mail/NinjaMailerJob.php | 2 +- app/Services/Email/Email.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 1124fec9c812..18e4304943a3 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -254,7 +254,7 @@ class NinjaMailerJob implements ShouldQueue private function incrementEmailCounter(): void { - if(in_array($this->mailer, ['default','mailgun','postmark'])) { + if(in_array($this->nmo->settings->email_sending_method, ['default','mailgun','postmark'])) { Cache::increment("email_quota".$this->company->account->key); } diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index 77edc44542eb..5202a92dbcfd 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -250,7 +250,7 @@ class Email implements ShouldQueue private function incrementEmailCounter(): void { - if(in_array($this->mailer, ['default','mailgun','postmark'])) { + if(in_array($this->email_object->settings->email_sending_method, ['default','mailgun','postmark'])) { Cache::increment("email_quota".$this->company->account->key); } } From 344603123a5eb20a21fdf0c4073c290a8e013389 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 11 Aug 2024 09:27:13 +1000 Subject: [PATCH 6/7] Small fixes for payment email logic --- app/Jobs/Payment/EmailPayment.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/Jobs/Payment/EmailPayment.php b/app/Jobs/Payment/EmailPayment.php index 1e93c099181d..5906cce74b17 100644 --- a/app/Jobs/Payment/EmailPayment.php +++ b/app/Jobs/Payment/EmailPayment.php @@ -58,10 +58,6 @@ class EmailPayment implements ShouldQueue */ public function handle() { - if ($this->company->is_disabled || (!$this->contact?->email ?? false)) { - nlog("company disabled - or - contact email not found"); - return; - } MultiDB::setDb($this->company->db); @@ -71,6 +67,11 @@ class EmailPayment implements ShouldQueue $this->contact = $this->payment->client->contacts()->orderBy('is_primary', 'desc')->first(); } + if ($this->company->is_disabled || (!$this->contact?->email ?? false)) { + nlog("company disabled - or - contact email not found"); + return; + } + $this->contact->load('client'); $email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build(); From 92f1ef98d0562a98aa82f810767ba5e91b340914 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Aug 2024 08:37:23 +1000 Subject: [PATCH 7/7] Add in task item log entry to task exports --- app/Export/CSV/BaseExport.php | 1 + app/Export/CSV/TaskExport.php | 5 ++++- app/Http/Requests/Task/UpdateTaskRequest.php | 4 ++-- app/Jobs/Ninja/AdjustEmailQuota.php | 4 ++-- .../ninja2020/dashboard/index.blade.php | 7 +++++++ tests/Feature/TaskApiTest.php | 19 +++++++++++++++++++ 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 4ea22fe974ee..0fdee8ad6394 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -451,6 +451,7 @@ class BaseExport 'project' => 'task.project_id', 'billable' => 'task.billable', 'item_notes' => 'task.item_notes', + 'time_log' => 'task.time_log', ]; protected array $forced_client_fields = [ diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 155d61a88f48..3d48b845a240 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -156,7 +156,7 @@ class TaskExport extends BaseExport $entity[$key] = $transformed_entity[$parts[1]]; } elseif (array_key_exists($key, $transformed_entity)) { $entity[$key] = $transformed_entity[$key]; - } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) { + } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes', 'task.time_log'])) { $entity[$key] = ''; } else { $entity[$key] = $this->decorator->transform($key, $task); @@ -207,6 +207,9 @@ class TaskExport extends BaseExport $seconds = $task->calcDuration(); $entity['task.duration'] = $seconds; $entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s'); + + $entity['task.time_log'] = (isset($item[1]) && $item[1] != 0) ? $item[1] - $item[0] : ctrans('texts.is_running'); + } if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) { diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 0944fb4eee1e..7e9752d29c20 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -67,7 +67,7 @@ class UpdateTaskRequest extends Request if(is_string($values)) { $values = json_decode($values, true); } - + if(!is_array($values)) { $fail('The '.$attribute.' must be a valid array.'); return; @@ -133,7 +133,7 @@ class UpdateTaskRequest extends Request } - if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}') { + if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}' || $input['time_log'] == '[""]') { $input['time_log'] = json_encode([]); } diff --git a/app/Jobs/Ninja/AdjustEmailQuota.php b/app/Jobs/Ninja/AdjustEmailQuota.php index 31a9dc1f2777..62ef46f490b2 100644 --- a/app/Jobs/Ninja/AdjustEmailQuota.php +++ b/app/Jobs/Ninja/AdjustEmailQuota.php @@ -79,7 +79,7 @@ class AdjustEmailQuota implements ShouldQueue /** Use redis pipelines to execute bulk deletes efficiently */ $redis = Redis::connection('sentinel-cache'); - $prefix = config('cache.prefix'). ":email_quota*"; + $prefix = config('cache.prefix'). "email_quota*"; $keys = $redis->keys($prefix); @@ -92,7 +92,7 @@ class AdjustEmailQuota implements ShouldQueue } $keys = null; - $prefix = config('cache.prefix'). ":throttle_notified*"; + $prefix = config('cache.prefix'). "throttle_notified*"; $keys = $redis->keys($prefix); diff --git a/resources/views/portal/ninja2020/dashboard/index.blade.php b/resources/views/portal/ninja2020/dashboard/index.blade.php index 8bd9314ca76e..21d8919bfd76 100644 --- a/resources/views/portal/ninja2020/dashboard/index.blade.php +++ b/resources/views/portal/ninja2020/dashboard/index.blade.php @@ -2,6 +2,13 @@ @section('meta_title', ctrans('texts.dashboard')) @section('body') + + @if($client->getSetting('custom_message_dashboard')) + @component('portal.ninja2020.components.message') +
{{ $client->getSetting('custom_message_dashboard') }}
+ @endcomponent + @endif +

{{ $contact->first_name }} {{ $contact->last_name }}

diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 801201b5876a..2db1a6db9f6b 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -224,6 +224,25 @@ class TaskApiTest extends TestCase ])->postJson("/api/v1/tasks", $data); $response->assertStatus(200); + $arr = $response->json(); + + $data = [ + 'client_id' => $this->client->hashed_id, + 'description' => 'Test Task', + 'time_log' => '[""]', + 'assigned_user' => [], + 'project' => [], + 'user' => [], + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson("/api/v1/tasks/".$arr['data']['id'], $data); + + $response->assertStatus(200); + } public function testUserFilters()