diff --git a/app/DataMapper/InvoiceItem.php b/app/DataMapper/InvoiceItem.php index bf7b1513bffa..60d1c0ee4ca4 100644 --- a/app/DataMapper/InvoiceItem.php +++ b/app/DataMapper/InvoiceItem.php @@ -64,7 +64,9 @@ class InvoiceItem public $task_id = ''; public $expense_id = ''; - + + public $unit_code = 'C62'; + public static $casts = [ 'task_id' => 'string', 'expense_id' => 'string', @@ -92,5 +94,6 @@ class InvoiceItem 'custom_value2' => 'string', 'custom_value3' => 'string', 'custom_value4' => 'string', + 'unit_code' => 'string', ]; } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index e5098f1876d0..59986bced072 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -250,6 +250,14 @@ class ClientController extends BaseController return response()->json(['message' => $hash_or_response], 200); } + if($action == 'assign_group' && $user->can('edit', $clients->first())){ + + $this->client_repo->assignGroup($clients, $request->group_settings_id); + + return $this->listResponse(Client::query()->withTrashed()->company()->whereIn('id', $request->ids)); + + } + $clients->each(function ($client) use ($action, $user) { if ($user->can('edit', $client)) { $this->client_repo->{$action}($client); diff --git a/app/Http/Controllers/CompanyGatewayController.php b/app/Http/Controllers/CompanyGatewayController.php index 8d69b3738a1c..fab62b6164c1 100644 --- a/app/Http/Controllers/CompanyGatewayController.php +++ b/app/Http/Controllers/CompanyGatewayController.php @@ -565,9 +565,11 @@ class CompanyGatewayController extends BaseController public function importCustomers(TestCompanyGatewayRequest $request, CompanyGateway $company_gateway) { + // $x = Cache::pull("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}"); + //Throttle here - // if (Cache::get("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) - // return response()->json(['message' => ctrans('texts.import_started')], 200); + if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) + return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200); dispatch(function () use($company_gateway) { MultiDB::setDb($company_gateway->company->db); diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php index e0db0722c133..b20285992506 100644 --- a/app/Http/Controllers/CreditController.php +++ b/app/Http/Controllers/CreditController.php @@ -781,7 +781,7 @@ class CreditController extends BaseController $contact = $invitation->contact; $credit = $invitation->credit; - $file = $credit->service()->getEInvoice($contact); + $file = $credit->service()->getECredit($contact); $file_name = $credit->getFileName("xml"); $headers = ['Content-Type' => 'application/xml']; diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 1682380e02f1..4b5fa3981b3b 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -646,7 +646,6 @@ class PurchaseOrderController extends BaseController echo $file; }, $purchase_order->numberFormatter().".pdf", ['Content-Type' => 'application/pdf']); - break; case 'restore': $this->purchase_order_repository->restore($purchase_order); diff --git a/app/Http/Requests/Client/BulkClientRequest.php b/app/Http/Requests/Client/BulkClientRequest.php index bfe6a91cdde7..dbfad9556e35 100644 --- a/app/Http/Requests/Client/BulkClientRequest.php +++ b/app/Http/Requests/Client/BulkClientRequest.php @@ -35,10 +35,11 @@ class BulkClientRequest extends Request $user = auth()->user(); return [ - 'action' => 'required|string|in:archive,restore,delete,template', + 'action' => 'required|string|in:archive,restore,delete,template,assign_group', 'ids' => ['required','bail','array',Rule::exists('clients', 'id')->where('company_id', $user->company()->id)], 'template' => 'sometimes|string', 'template_id' => 'sometimes|string', + 'group_settings_id' => ['required_if:action,assign_group',Rule::exists('group_settings', 'id')->where('company_id', $user->company()->id)], 'send_email' => 'sometimes|bool' ]; @@ -52,6 +53,10 @@ class BulkClientRequest extends Request $input['ids'] = $this->transformKeys($input['ids']); } + if (isset($input['group_settings_id'])) { + $input['group_settings_id'] = $this->decodePrimaryKey($input['group_settings_id']); + } + $this->replace($input); } } diff --git a/app/Jobs/Cron/SubscriptionCron.php b/app/Jobs/Cron/SubscriptionCron.php index 6e29ba088384..a3a5286a3816 100644 --- a/app/Jobs/Cron/SubscriptionCron.php +++ b/app/Jobs/Cron/SubscriptionCron.php @@ -39,62 +39,25 @@ class SubscriptionCron */ public function handle(): void { - nlog('Subscription Cron'); Auth::logout(); if (! config('ninja.db.multi_db_enabled')) { - $invoices = Invoice::where('is_deleted', 0) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('balance', '>', 0) - ->where('is_proforma', 0) - ->whereDate('due_date', '<=', now()->addDay()->startOfDay()) - ->whereNull('deleted_at') - ->whereNotNull('subscription_id') - ->cursor(); - $invoices->each(function (Invoice $invoice) { - $subscription = $invoice->subscription; + nlog('Subscription Cron '. now()->toDateTimeString()); - $body = [ - 'context' => 'plan_expired', - 'client' => $invoice->client->hashed_id, - 'invoice' => $invoice->hashed_id, - 'subscription' => $subscription->hashed_id, - ]; + $this->timezoneAware(); - $this->sendLoad($subscription, $body); - //This will send the notification daily. - //We'll need to handle this by performing some action on the invoice to either archive it or delete it? - }); + } else { //multiDB environment, need to foreach (MultiDB::$dbs as $db) { MultiDB::setDB($db); - $invoices = Invoice::where('is_deleted', 0) - ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) - ->where('balance', '>', 0) - ->where('is_proforma', 0) - ->whereDate('due_date', '<=', now()->addDay()->startOfDay()) - ->whereNull('deleted_at') - ->whereNotNull('subscription_id') - ->cursor(); + nlog('Subscription Cron for ' . $db . ' ' . now()->toDateTimeString()); - $invoices->each(function (Invoice $invoice) { - $subscription = $invoice->subscription; + $this->timezoneAware(); - $body = [ - 'context' => 'plan_expired', - 'client' => $invoice->client->hashed_id, - 'invoice' => $invoice->hashed_id, - 'subscription' => $subscription->hashed_id, - ]; - - $this->sendLoad($subscription, $body); - //This will send the notification daily. - //We'll need to handle this by performing some action on the invoice to either archive it or delete it? - }); } } } @@ -131,7 +94,7 @@ class SubscriptionCron ->where('is_proforma', 0) ->whereNotNull('subscription_id') ->where('balance', '>', 0) - ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->whereDate('due_date', '<=', now()->addDay()->startOfDay()) ->cursor() ->each(function (Invoice $invoice) { diff --git a/app/Jobs/Entity/CreateRawPdf.php b/app/Jobs/Entity/CreateRawPdf.php index bb5a37001283..ac4320f1a89d 100644 --- a/app/Jobs/Entity/CreateRawPdf.php +++ b/app/Jobs/Entity/CreateRawPdf.php @@ -88,6 +88,7 @@ class CreateRawPdf 'quote' => $type = 'product', 'credit' => $type = 'product', 'recurring_invoice' => $type = 'product', + default => $type = 'product', }; return $type; diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index bff1f3f5aa7c..8ec580d0d4b0 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -70,9 +70,7 @@ class NinjaMailerJob implements ShouldQueue public function backoff() { - // return [5, 10, 30, 240]; - return [rand(5, 10), rand(30, 40), rand(60, 79), rand(160, 400)]; - + return [rand(5, 29), rand(30, 59), rand(61, 100), rand(180, 500)]; } public function handle() @@ -182,6 +180,11 @@ class NinjaMailerJob implements ShouldQueue $this->fail(); $this->logMailError($e->getMessage(), $this->company->clients()->first()); + + if ($this->nmo->entity) { + $this->entityEmailFailed($message); + } + $this->cleanUpMailers(); return; @@ -195,6 +198,11 @@ class NinjaMailerJob implements ShouldQueue $this->fail(); $this->logMailError($message, $this->company->clients()->first()); + + if ($this->nmo->entity) { + $this->entityEmailFailed($message); + } + $this->cleanUpMailers(); return; @@ -203,7 +211,7 @@ class NinjaMailerJob implements ShouldQueue //only report once, not on all tries if ($this->attempts() == $this->tries) { - /* If the is an entity attached to the message send a failure mailer */ + /* If there is an entity attached to the message send a failure mailer */ if ($this->nmo->entity) { $this->entityEmailFailed($message); } diff --git a/app/Models/Account.php b/app/Models/Account.php index adf95c62086f..c2e099bf8366 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -291,7 +291,7 @@ class Account extends BaseModel public function isPaid(): bool { - return Ninja::isNinja() ? ($this->isPaidHostedClient() && !$this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL); + return Ninja::isNinja() ? $this->isPaidHostedClient() : $this->hasFeature(self::FEATURE_WHITE_LABEL); } public function isPremium(): bool diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 95be95a9123f..126e405ed03b 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -190,7 +190,7 @@ class Expense extends BaseModel public function purchase_order() { - return $this->hasOne(PurchaseOrder::class); + return $this->hasOne(PurchaseOrder::class)->withTrashed(); } public function translate_entity() diff --git a/app/Models/Product.php b/app/Models/Product.php index 7c49afc9338f..55ffa44e0de3 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -99,6 +99,64 @@ class Product extends BaseModel 'tax_id', ]; + public array $ubl_tax_map = [ + self::PRODUCT_TYPE_REVERSE_TAX => 'AE', // VAT_REVERSE_CHARGE = + self::PRODUCT_TYPE_EXEMPT => 'E', // EXEMPT_FROM_TAX = + self::PRODUCT_TYPE_PHYSICAL => 'S', // STANDARD_RATE = + self::PRODUCT_TYPE_ZERO_RATED => 'Z', // ZERO_RATED_GOODS = + // self::PRODUCT_TYPE_ZERO_RATED => 'G', // FREE_EXPORT_ITEM = + // self::PRODUCT_TYPE_ZERO_RATED => 'O', // OUTSIDE_TAX_SCOPE = + // self::PRODUCT_TYPE_EXEMPT => 'K', // EEA_GOODS_AND_SERVICES = + // self::PRODUCT_TYPE_PHYSICAL => 'L', // CANARY_ISLANDS_INDIRECT_TAX = + // self::PRODUCT_TYPE_PHYSICAL => 'M', // CEUTA_AND_MELILLA = + // self::PRODUCT_TYPE_PHYSICAL => 'B', // TRANSFERRED_VAT_ITALY = + // self::PRODUCT_TYPE_PHYSICAL => 'A', // MIXED_TAX_RATE = + self::PRODUCT_TYPE_REDUCED_TAX => 'AA', // LOWER_RATE = + // self::PRODUCT_TYPE_PHYSICAL => 'AB', // EXEMPT_FOR_RESALE = + // self::PRODUCT_TYPE_PHYSICAL => 'AC', // VAT_NOT_NOW_DUE = + // self::PRODUCT_TYPE_PHYSICAL => 'AD', // VAT_DUE_PREVIOUS_INVOICE = + // self::PRODUCT_TYPE_PHYSICAL => 'B', // TRANSFERRED_VAT = + // self::PRODUCT_TYPE_PHYSICAL => 'C', // DUTY_PAID_BY_SUPPLIER = + // self::PRODUCT_TYPE_PHYSICAL => 'D', // VAT_MARGIN_SCHEME_TRAVEL_AGENTS = + // self::PRODUCT_TYPE_PHYSICAL => 'F', // VAT_MARGIN_SCHEME_SECOND_HAND_GOODS = + // self::PRODUCT_TYPE_PHYSICAL => 'H', // HIGHER_RATE = + // self::PRODUCT_TYPE_PHYSICAL => 'I', // VAT_MARGIN_SCHEME_WORKS_OF_ART = + // self::PRODUCT_TYPE_PHYSICAL => 'J', // VAT_MARGIN_SCHEME_COLLECTORS_ITEMS = + // self::PRODUCT_TYPE_PHYSICAL => 'K', // VAT_EXEMPT_EEA_INTRA_COMMUNITY = + // self::PRODUCT_TYPE_PHYSICAL => 'L', // CANARY_ISLANDS_TAX = + // self::PRODUCT_TYPE_PHYSICAL => 'M', // TAX_CEUTA_MELILLA = + // self::PRODUCT_TYPE_PHYSICAL => 'O', // SERVICES_OUTSIDE_SCOPE = + ]; + + public array $ubl_tax_translations = [ + 'texts.reverse_tax' => 'AE', // VAT_REVERSE_CHARGE + 'texts.tax_exempt' => 'E', // EXEMPT_FROM_TAX + 'texts.physical_goods' => 'S', // STANDARD_RATE + 'texts.zero_rated' => 'Z', // ZERO_RATED_GOODS + 'ubl.vat_exempt_eea_intra_community' => 'K', // VAT_EXEMPT_EEA_INTRA_COMMUNITY + 'ubl.free_export_item' => 'G', // FREE_EXPORT_ITEM + 'ubl.outside_tax_scope' => 'O', // OUTSIDE_TAX_SCOPE + 'ubl.eea_goods_and_services' => 'K', // EEA_GOODS_AND_SERVICES + 'ubl.canary_islands_indirect_tax' => 'L', // CANARY_ISLANDS_INDIRECT_TAX + 'ubl.ceuta_and_melilla' => 'M', // CEUTA_AND_MELILLA + 'ubl.transferred_vat_italy' => 'B', // TRANSFERRED_VAT_ITALY + 'ubl.mixed_tax_rate' => 'A', // MIXED_TAX_RATE + 'ubl.lower_rate' => 'AA', // LOWER_RATE + 'ubl.exempt_for_resale' => 'AB', // EXEMPT_FOR_RESALE + 'ubl.vat_not_now_due' => 'AC', // VAT_NOT_NOW_DUE + 'ubl.vat_due_previous_invoice' => 'AD', // VAT_DUE_PREVIOUS_INVOICE + 'ubl.transferred_vat' => 'B', // TRANSFERRED_VAT + 'ubl.duty_paid_by_supplier' => 'C', // DUTY_PAID_BY_SUPPLIER + 'ubl.vat_margin_scheme_travel_agents' => 'D', // VAT_MARGIN_SCHEME_TRAVEL_AGENTS + 'ubl.vat_margin_scheme_second_hand_goods' => 'F', // VAT_MARGIN_SCHEME_SECOND_HAND_GOODS + 'ubl.higher_rate' => 'H', // HIGHER_RATE + 'ubl.vat_margin_scheme_works_of_art' => 'I', // VAT_MARGIN_SCHEME_WORKS_OF_ART + 'ubl.vat_margin_scheme_collectors_items' => 'J', // VAT_MARGIN_SCHEME_COLLECTORS_ITEMS + 'ubl.canary_islands_tax' => 'L', // CANARY_ISLANDS_TAX + 'ubl.tax_ceuta_melilla' => 'M', // TAX_CEUTA_MELILLA + 'ubl.services_outside_scope' => 'O', // SERVICES_OUTSIDE_SCOPE + ]; + protected $touches = []; public function getEntityType() diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 3cc2b0e0de3b..284c211f56b5 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -265,7 +265,7 @@ class PurchaseOrder extends BaseModel public function expense(): \Illuminate\Database\Eloquent\Relations\BelongsTo { - return $this->belongsTo(Expense::class); + return $this->belongsTo(Expense::class)->withTrashed(); } public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo diff --git a/app/PaymentDrivers/Authorize/AuthorizeCustomer.php b/app/PaymentDrivers/Authorize/AuthorizeCustomer.php index a9fd927294da..9a4d2f5765b1 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeCustomer.php +++ b/app/PaymentDrivers/Authorize/AuthorizeCustomer.php @@ -103,7 +103,7 @@ class AuthorizeCustomer } else { // nlog("creating client"); - $first_payment_profile = $profile['payment_profiles'][0]; + $first_payment_profile = &$profile['payment_profiles'][0]; if (! $first_payment_profile) { continue; diff --git a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php index 22eac01927a0..07b7ea7516f5 100644 --- a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php +++ b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php @@ -101,9 +101,10 @@ class AuthorizePaymentMethod $gateway_customer_reference = (new AuthorizeCreateCustomer($this->authorize, $this->authorize->client))->create($data); $payment_profile = $this->addPaymentMethodToClient($gateway_customer_reference, $data); - $this->createClientGatewayToken($payment_profile, $gateway_customer_reference); } + $this->createClientGatewayToken($payment_profile, $gateway_customer_reference); + return redirect()->route('client.payment_methods.index'); } @@ -168,6 +169,9 @@ class AuthorizePaymentMethod $billto->setCity(substr($this->authorize->client->city, 0, 40)); $billto->setState(substr($this->authorize->client->state, 0, 40)); $billto->setZip(substr($this->authorize->client->postal_code, 0, 20)); + + if(isset($contact->email) && str_contains($contact->email, '@')) + $billto->setEmail($contact->email); if ($this->authorize->client->country_id) { $billto->setCountry($this->authorize->client->country->name); @@ -179,7 +183,7 @@ class AuthorizePaymentMethod // Create a new Customer Payment Profile object $paymentprofile = new CustomerPaymentProfileType(); $paymentprofile->setCustomerType('individual'); - + if ($billto) { $paymentprofile->setBillTo($billto); } diff --git a/app/PaymentDrivers/AuthorizePaymentDriver.php b/app/PaymentDrivers/AuthorizePaymentDriver.php index 87c74fdb9209..0e8c820d133f 100644 --- a/app/PaymentDrivers/AuthorizePaymentDriver.php +++ b/app/PaymentDrivers/AuthorizePaymentDriver.php @@ -195,6 +195,8 @@ class AuthorizePaymentDriver extends BaseDriver { $this->init(); + nlog("starting import auth.net"); + return (new AuthorizeCustomer($this))->importCustomers(); } diff --git a/app/Repositories/ActivityRepository.php b/app/Repositories/ActivityRepository.php index 35465a3acf7a..09c530d7ad1b 100644 --- a/app/Repositories/ActivityRepository.php +++ b/app/Repositories/ActivityRepository.php @@ -41,7 +41,7 @@ class ActivityRepository extends BaseRepository * Save the Activity. * * @param \stdClass $fields The fields - * @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder $entity + * @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\Expense $entity * @param array $event_vars */ public function save($fields, $entity, $event_vars) @@ -72,7 +72,7 @@ class ActivityRepository extends BaseRepository /** * Creates a backup. * - * @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder $entity + * @param \App\Models\Invoice | \App\Models\Quote | \App\Models\Credit | \App\Models\PurchaseOrder | \App\Models\Expense $entity * @param \App\Models\Activity $activity The activity */ public function createBackup($entity, $activity) diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php index f7ec59ce7fba..3083a2314df9 100644 --- a/app/Repositories/ClientRepository.php +++ b/app/Repositories/ClientRepository.php @@ -126,6 +126,21 @@ class ClientRepository extends BaseRepository ClientFactory::create($user->company()->id, $user->id) ); } + + /** + * Bulk assign clients to a group. + * + * @param mixed $clients + * @param mixed $group_settings_id + * @return void + */ + public function assignGroup($clients, $group_settings_id): void + { + Client::query() + ->company() + ->whereIn('id', $clients->pluck('id')) + ->update(['group_settings_id' => $group_settings_id]); + } public function purge($client) { diff --git a/app/Repositories/ExpenseRepository.php b/app/Repositories/ExpenseRepository.php index 5b87af81466d..b9d125045fff 100644 --- a/app/Repositories/ExpenseRepository.php +++ b/app/Repositories/ExpenseRepository.php @@ -46,10 +46,12 @@ class ExpenseRepository extends BaseRepository /** @var \App\Models\User $user */ $user = auth()->user(); - if(isset($data['payment_date']) && $data['payment_date'] == $expense->payment_date) { + $payment_date = &$data['payment_date']; + $vendor_id = &$data['vendor_id']; + + if($payment_date && $payment_date == $expense->payment_date) { //do nothing - } elseif(isset($data['payment_date']) && strlen($data['payment_date']) > 1 && $user->company()->notify_vendor_when_paid && (isset($data['vendor_id']) || $expense->vendor_id)) { - nlog("ping"); + } elseif($payment_date && strlen($payment_date) > 1 && $user->company()->notify_vendor_when_paid && ($vendor_id || $expense->vendor_id)) { $this->notify_vendor = true; } @@ -73,6 +75,13 @@ class ExpenseRepository extends BaseRepository VendorExpenseNotify::dispatch($expense, $expense->company->db); } + if($payment_date && strlen($payment_date) > 1 && $expense->purchase_order) { + $purchase_order = $expense->purchase_order; + $purchase_order->balance = round($purchase_order->amount - $expense->amount, 2); + $purchase_order->paid_to_date = $expense->amount; + $purchase_order->save(); + } + return $expense; } diff --git a/app/Services/EDocument/Samples/ro.xml b/app/Services/EDocument/Samples/ro.xml new file mode 100644 index 000000000000..cc2d5e8136b1 --- /dev/null +++ b/app/Services/EDocument/Samples/ro.xml @@ -0,0 +1,119 @@ + + + 2.1 + urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1 + ABC 0020 + 2024-01-01 + 2024-01-15 + 384 + RON + RON + + + + 234234234 + + + This can be the full address , not just the street and street nr. + SECTOR2 + RO-B + + RO + + + + RO234234234 + + VAT + + + + Some Copany Name + J40/2222/2009 + + + Someone + 88282819832 + some@email.com + + + + + + + 646546549 + + + This can be the full address , not just the street and street nr. + SECTOR3 + RO-B + + RO + + + + Some Comapny + 646546549 + + + Someone + + some@email.com + + + + + 42 + + some account nr + Bank name + + + + 63.65 + + 335.00 + 63.65 + + S // this is a speciffic identifier for the VAT type + 19 + + VAT + + + + + + 335.00 + 335.00 + 398.65 + 0.00 + 398.65 + + + 1 + 1 // unitcode + is a speciffic identifier for the type of product + 335.00 + + Some Description + Some product + + S // this is a speciffic identifier for the VAT type + 19 + + VAT + + + + + 335 + + + \ No newline at end of file diff --git a/app/Services/EDocument/Standards/RoEInvoice.php b/app/Services/EDocument/Standards/RoEInvoice.php index d9945e9834df..6cdf2885d731 100644 --- a/app/Services/EDocument/Standards/RoEInvoice.php +++ b/app/Services/EDocument/Standards/RoEInvoice.php @@ -31,6 +31,7 @@ use CleverIt\UBL\Invoice\TaxCategory; use CleverIt\UBL\Invoice\TaxScheme; use CleverIt\UBL\Invoice\TaxSubTotal; use CleverIt\UBL\Invoice\TaxTotal; +use App\Models\Product; class RoEInvoice extends AbstractService { @@ -65,10 +66,10 @@ class RoEInvoice extends AbstractService $ubl_invoice = new UBLInvoice(); // invoice - $ubl_invoice->setId($invoice->custom_value1 . ' ' . $invoice->number); + $ubl_invoice->setId($invoice->number); $ubl_invoice->setIssueDate(date_create($invoice->date)); $ubl_invoice->setDueDate(date_create($invoice->due_date)); - $ubl_invoice->setInvoiceTypeCode(explode('-', $invoice->custom_value3)[0]); + $ubl_invoice->setInvoiceTypeCode("380"); $ubl_invoice->setDocumentCurrencyCode($invoice->client->getCurrencyCode()); $ubl_invoice->setTaxCurrencyCode($invoice->client->getCurrencyCode()); @@ -130,7 +131,7 @@ class RoEInvoice extends AbstractService ->setTaxAmount($invoicing_data->getItemTotalTaxes()) ->setTaxableAmount($taxable) ->setTaxCategory((new TaxCategory()) - ->setId(explode('-', $company->settings->custom_value3)[0]) + ->setId("S") ->setPercent($taxRatePercent) ->setTaxScheme(($taxNameScheme === 'TVA') ? 'VAT' : $taxNameScheme))); $ubl_invoice->setTaxTotal($taxtotal); @@ -212,29 +213,29 @@ class RoEInvoice extends AbstractService { if (strlen($item->tax_name1) > 1) { $classifiedTaxCategory = (new ClassifiedTaxCategory()) - ->setId(explode('-', $item->custom_value4)[0]) + ->setId($this->resolveTaxCode($item->tax_id ?? 1)) ->setPercent($item->tax_rate1) ->setTaxScheme(($item->tax_name1 === 'TVA') ? 'VAT' : $item->tax_name1); } elseif (strlen($item->tax_name2) > 1) { $classifiedTaxCategory = (new ClassifiedTaxCategory()) - ->setId(explode('-', $item->custom_value4)[0]) + ->setId($this->resolveTaxCode($item->tax_id ?? 1)) ->setPercent($item->tax_rate2) ->setTaxScheme(($item->tax_name2 === 'TVA') ? 'VAT' : $item->tax_name2); } elseif (strlen($item->tax_name3) > 1) { $classifiedTaxCategory = (new ClassifiedTaxCategory()) - ->setId(explode('-', $item->custom_value4)[0]) + ->setId($this->resolveTaxCode($item->tax_id ?? 1)) ->setPercent($item->tax_rate3) ->setTaxScheme(($item->tax_name3 === 'TVA') ? 'VAT' : $item->tax_name3); } $invoiceLine = (new InvoiceLine()) ->setId($index + 1) ->setInvoicedQuantity($item->quantity) - ->setUnitCode($item->custom_value3) + ->setUnitCode($item->unit_code ?? 'C62') ->setLineExtensionAmount($item->line_total) ->setItem((new Item()) ->setName($item->product_key) ->setDescription($item->notes) - ->setClassifiedTaxCategory($classifiedTaxCategory)) + ->setClassifiedTaxCategory([$classifiedTaxCategory])) ->setPrice((new Price()) ->setPriceAmount($this->costWithDiscount($item))); @@ -365,6 +366,25 @@ class RoEInvoice extends AbstractService } } + private function resolveTaxCode($tax_id) + { + $code = $tax_id; + + match($tax_id){ + Product::PRODUCT_TYPE_REVERSE_TAX => $code = 'AE', // VAT_REVERSE_CHARGE = + Product::PRODUCT_TYPE_EXEMPT => $code = 'E', // EXEMPT_FROM_TAX = + Product::PRODUCT_TYPE_PHYSICAL => $code = 'S', // STANDARD_RATE = + Product::PRODUCT_TYPE_ZERO_RATED => $code = 'Z', // ZERO_RATED_GOODS = + Product::PRODUCT_TYPE_REDUCED_TAX => $code = 'AA', // LOWER_RATE = + Product::PRODUCT_TYPE_SERVICE => $code = 'S', // STANDARD_RATE = + Product::PRODUCT_TYPE_DIGITAL => $code = 'S', // STANDARD_RATE = + Product::PRODUCT_TYPE_SHIPPING => $code = 'S', // STANDARD_RATE = + Product::PRODUCT_TYPE_OVERRIDE_TAX => $code = 'S', // STANDARD_RATE = + }; + + return $code; + } + public function generateXml(): string { $ubl_invoice = $this->run(); // Call the existing handle method to get the UBLInvoice diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index c07d34515a0f..96faccf0d576 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -93,7 +93,7 @@ class Email implements ShouldQueue */ public function backoff() { - return [rand(10, 20), rand(30, 45), rand(60, 79), rand(160, 400)]; + return [rand(5, 29), rand(30, 59), rand(61, 100), rand(180, 500)]; } /** @@ -314,6 +314,8 @@ class Email implements ShouldQueue $this->logMailError($e->getMessage(), $this->company->clients()->first()); $this->cleanUpMailers(); +$this->entityEmailFailed($message); + return; } @@ -329,6 +331,8 @@ class Email implements ShouldQueue $this->logMailError($message, $this->company->clients()->first()); $this->cleanUpMailers(); + $this->entityEmailFailed($message); + return; } @@ -343,11 +347,12 @@ class Email implements ShouldQueue if ($message_body && property_exists($message_body, 'Message')) { $message = $message_body->Message; - nlog($message); } $this->fail(); + $this->entityEmailFailed($message); $this->cleanUpMailers(); + return; } diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 5346c0636fe6..19121cc12014 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -301,7 +301,7 @@ class EmailDefaults $documents = []; /* Return early if the user cannot attach documents */ - if (!$this->email->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT) || $this->email->email_object->email_template_subject == 'email_subject_statement') { + if (!$this->email->email_object->invitation || !$this->email->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT) || $this->email->email_object->email_template_subject == 'email_subject_statement') { return $this; } diff --git a/app/Services/Invoice/GenerateDeliveryNote.php b/app/Services/Invoice/GenerateDeliveryNote.php index c8a3a85b615f..1aa96da0a41c 100644 --- a/app/Services/Invoice/GenerateDeliveryNote.php +++ b/app/Services/Invoice/GenerateDeliveryNote.php @@ -49,7 +49,9 @@ class GenerateDeliveryNote if($design && $design->is_template) { $ts = new TemplateService($design); - $pdf = $ts->build([ + + $pdf = $ts->setCompany($this->invoice->company) + ->build([ 'invoices' => collect([$this->invoice]), ])->getPdf(); diff --git a/lang/en/texts.php b/lang/en/texts.php index 8ba1bf2aa2aa..eb6aea8fd1e5 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -1173,8 +1173,8 @@ $lang = array( 'invoice_number_padding' => 'Padding', 'preview' => 'Preview', 'list_vendors' => 'List Vendors', - 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', - 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments, :link to see the full list of features.', + 'add_users_not_supported' => 'Upgrade to the Enterprise Plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise Plan adds support for multiple users and file attachments, :link to see the full list of features.', 'return_to_app' => 'Return To App', @@ -1323,7 +1323,7 @@ $lang = array( 'security' => 'Security', 'see_whats_new' => 'See what\'s new in v:version', 'wait_for_upload' => 'Please wait for the document upload to complete.', - 'upgrade_for_permissions' => 'Upgrade to our Enterprise plan to enable permissions.', + 'upgrade_for_permissions' => 'Upgrade to our Enterprise Plan to enable permissions.', 'enable_second_tax_rate' => 'Enable specifying a second tax rate', 'payment_file' => 'Payment File', 'expense_file' => 'Expense File', @@ -2697,7 +2697,7 @@ $lang = array( 'no_assets' => 'No images, drag to upload', 'add_image' => 'Add Image', 'select_image' => 'Select Image', - 'upgrade_to_upload_images' => 'Upgrade to the enterprise plan to upload images', + 'upgrade_to_upload_images' => 'Upgrade to the Enterprise Plan to upload images', 'delete_image' => 'Delete Image', 'delete_image_help' => 'Warning: deleting the image will remove it from all proposals.', 'amount_variable_help' => 'Note: the invoice $amount field will use the partial/deposit field if set otherwise it will use the invoice balance.', @@ -3053,7 +3053,7 @@ $lang = array( 'valid_until_days' => 'Valid Until', 'valid_until_days_help' => 'Automatically sets the Valid Until value on quotes to this many days in the future. Leave blank to disable.', 'usually_pays_in_days' => 'Days', - 'requires_an_enterprise_plan' => 'Requires an enterprise plan', + 'requires_an_enterprise_plan' => 'Requires an Enterprise Plan', 'take_picture' => 'Take Picture', 'upload_file' => 'Upload File', 'new_document' => 'New Document', @@ -3155,7 +3155,7 @@ $lang = array( 'archived_group' => 'Successfully archived group', 'deleted_group' => 'Successfully deleted group', 'restored_group' => 'Successfully restored group', - 'upload_logo' => 'Upload Logo', + 'upload_logo' => 'Upload Your Company Logo', 'uploaded_logo' => 'Successfully uploaded logo', 'saved_settings' => 'Successfully saved settings', 'device_settings' => 'Device Settings', @@ -3977,7 +3977,7 @@ $lang = array( 'notification_credit_bounced_subject' => 'Unable to deliver Credit :invoice', 'save_payment_method_details' => 'Save payment method details', 'new_card' => 'New card', - 'new_bank_account' => 'New bank account', + 'new_bank_account' => 'Add Bank Account', 'company_limit_reached' => 'Limit of :limit companies per account.', 'credits_applied_validation' => 'Total credits applied cannot be MORE than total of invoices', 'credit_number_taken' => 'Credit number already taken', @@ -5196,7 +5196,7 @@ $lang = array( 'nordigen_handler_error_heading_account_config_invalid' => 'Missing Credentials', 'nordigen_handler_error_contents_account_config_invalid' => 'Invalid or missing credentials for Gocardless Bank Account Data. Contact support for help, if this issue persists.', 'nordigen_handler_error_heading_not_available' => 'Not Available', - 'nordigen_handler_error_contents_not_available' => 'Feature unavailable, enterprise plan only.', + 'nordigen_handler_error_contents_not_available' => 'Feature unavailable, Enterprise Plan only.', 'nordigen_handler_error_heading_institution_invalid' => 'Invalid Institution', 'nordigen_handler_error_contents_institution_invalid' => 'The provided institution-id is invalid or no longer valid.', 'nordigen_handler_error_heading_ref_invalid' => 'Invalid Reference', @@ -5288,6 +5288,7 @@ $lang = array( 'show_table_footer' => 'Show table footer', 'show_table_footer_help' => 'Displays the totals in the footer of the table', 'total_invoices' => 'Total Invoices', + 'add_to_group' => 'Add to group', ); return $lang; diff --git a/lang/en/ubl.php b/lang/en/ubl.php new file mode 100644 index 000000000000..3178e5b10cda --- /dev/null +++ b/lang/en/ubl.php @@ -0,0 +1,28 @@ + 'Free export item', + 'outside_tax_scope' => 'Outside tax scope', + 'eea_goods_and_services' => 'EEA goods and services', + 'lower_rate' => 'Lower rate', + 'mixed_tax_rate' => 'Mixed tax rate', + 'higher_rate' => 'Higher rate', + 'canary_islands_indirect_tax' => 'Canary Islands indirect tax', + 'ceuta_and_melilla' => 'Ceuta and Melilla', + 'transferred_vat_italy' => 'Transferred VAT Italy', + 'exempt_for_resale' => 'Exempt for resale', + 'vat_not_now_due' => 'VAT not now due', + 'vat_due_previous_invoice' => 'VAT due previous', + 'transferred_vat' => 'Transferred VAT', + 'duty_paid_by_supplier' => 'Duty paid by supplier', + 'vat_margin_scheme_travel_agents' => 'VAT margin scheme travel agents', + 'vat_margin_scheme_second_hand_goods' => 'VAT margin scheme second hand goods', + 'vat_margin_scheme_works_of_art' => 'VAT margin scheme works of art', + 'vat_margin_scheme_collectors_items' => 'VAT margin scheme collectors items', + 'vat_exempt_eea_intra_community' => 'VAT exempt EEA intra community', + 'canary_islands_tax' => 'Canary Islands tax', + 'tax_ceuta_melilla' => 'Tax Ceuta Melilla', + 'services_outside_scope' => 'Services outside scope', +); + +return $lang; \ No newline at end of file diff --git a/resources/views/pdf-designs/plain.html b/resources/views/pdf-designs/plain.html index a2db2c000911..61def4c09c07 100644 --- a/resources/views/pdf-designs/plain.html +++ b/resources/views/pdf-designs/plain.html @@ -101,7 +101,7 @@ flex-direction: column; line-height: var(--line-height); white-space: nowrap; - border: 1px solid #000; + border: 0px solid #000; } [data-ref="table"] { diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php index e2850a2615df..44514d00f220 100644 --- a/tests/Feature/ClientTest.php +++ b/tests/Feature/ClientTest.php @@ -21,12 +21,15 @@ use App\Models\Currency; use Tests\MockAccountData; use Illuminate\Support\Str; use App\Models\CompanyToken; +use App\Models\GroupSetting; use App\Models\ClientContact; use App\Utils\Traits\MakesHash; +use Tests\Unit\GroupSettingsTest; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; use App\DataMapper\DefaultSettings; use App\Factory\InvoiceItemFactory; +use App\Factory\GroupSettingFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; @@ -69,6 +72,45 @@ class ClientTest extends TestCase $this->makeTestData(); } + public function testBulkGroupAssignment() + { + Client::factory()->count(5)->create(['user_id' => $this->user->id, 'company_id' => $this->company->id])->each(function ($c) { + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + }); + + $gs = GroupSettingFactory::create($this->company->id, $this->user->id); + $gs->name = 'testtest'; + $gs->save(); + + $ids = Client::where('company_id', $this->company->id)->get()->pluck('hashed_id')->toArray(); + $data = [ + 'action' => 'assign_group', + 'ids' => $ids, + 'group_settings_id' => $gs->hashed_id, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/clients/bulk', $data); + + $arr = $response->json(); + + Client::query()->whereIn('id', $this->transformKeys($ids))->cursor()->each(function ($c) use ($gs, $arr) { + $this->assertEquals($gs->id, $c->group_settings_id); + }); + + foreach($arr['data'] as $client_response){ + + $this->assertEquals($gs->hashed_id, $client_response['group_settings_id']); + } + } + public function testClientExchangeRateCalculation() { $settings = ClientSettings::defaults(); diff --git a/tests/Feature/SubscriptionApiTest.php b/tests/Feature/SubscriptionApiTest.php index a0fb674d794a..f7b1bee1af20 100644 --- a/tests/Feature/SubscriptionApiTest.php +++ b/tests/Feature/SubscriptionApiTest.php @@ -12,13 +12,19 @@ namespace Tests\Feature; use Tests\TestCase; +use App\Models\User; +use App\Models\Client; +use App\Models\Company; use App\Models\Invoice; use App\Models\Product; -use App\Models\RecurringInvoice; use Tests\MockAccountData; use Illuminate\Support\Str; +use App\Models\CompanyToken; use App\Models\Subscription; use App\Utils\Traits\MakesHash; +use App\Models\RecurringInvoice; +use App\DataMapper\CompanySettings; +use App\Factory\CompanyUserFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; @@ -51,6 +57,250 @@ class SubscriptionApiTest extends TestCase Model::reguard(); } + public function testSubscriptionCronLocalization() + { + + $settings = CompanySettings::defaults(); + $settings->timezone_id = '50'; //europe/vienna + + $c2 = Company::factory()->create([ + 'account_id' => $this->company->account_id, + 'settings' => $settings + ]); + + $cu = CompanyUserFactory::create($this->user->id, $c2->id, $this->account->id); + $cu->is_owner = true; + $cu->is_admin = true; + $cu->is_locked = true; + $cu->permissions = '["view_client"]'; + $cu->save(); + + $different_company_token = \Illuminate\Support\Str::random(64); + + $company_token = new CompanyToken(); + $company_token->user_id = $this->user->id; + $company_token->company_id = $c2->id; + $company_token->account_id = $c2->account_id; + $company_token->name = 'test token'; + $company_token->token = $different_company_token; + $company_token->is_system = true; + $company_token->save(); + + + $s = Subscription::factory()->create([ + 'company_id' => $c2->id, + 'user_id' => $this->user->id, + ]); + + $client2 = Client::factory()->create([ + 'company_id' => $c2->id, + 'user_id' => $this->user->id, + ]); + + $i = Invoice::factory()->create([ + 'company_id' => $c2->id, + 'user_id' => $this->user->id, + 'subscription_id' => $s->id, + 'due_date' => now()->startOfDay(), + 'client_id' => $client2->id, + 'status_id' => Invoice::STATUS_SENT + ]); + + $settings = CompanySettings::defaults(); + $settings->timezone_id = '110'; //sydney/australia + + $c = Company::factory()->create([ + 'account_id' => $this->company->account_id, + 'settings' => $settings, + ]); + + $cu = CompanyUserFactory::create($this->user->id, $c->id, $this->account->id); + $cu->is_owner = true; + $cu->is_admin = true; + $cu->is_locked = true; + $cu->permissions = '["view_client"]'; + $cu->save(); + + $different_company_token = \Illuminate\Support\Str::random(64); + + $company_token = new CompanyToken(); + $company_token->user_id = $this->user->id; + $company_token->company_id = $c->id; + $company_token->account_id = $c->account_id; + $company_token->name = 'test token'; + $company_token->token = $different_company_token; + $company_token->is_system = true; + $company_token->save(); + + $s1 = Subscription::factory()->create([ + 'company_id' => $c->id, + 'user_id' => $this->user->id, + ]); + + + $client = Client::factory()->create([ + 'company_id' => $c2->id, + 'user_id' => $this->user->id, + ]); + + $i = Invoice::factory()->create([ + 'company_id' => $c->id, + 'user_id' => $this->user->id, + 'subscription_id' => $s1->id, + 'due_date' => now()->startOfDay(), + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT + ]); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + $company = Company::find($c->id); //sydney + + $timezone_now = now()->setTimezone($company->timezone()->name); + + $this->assertEquals('Australia/Sydney', $timezone_now->timezoneName); + + $this->travelTo($timezone_now->copy()->startOfDay()->subHour()); + + $i = false; + + //Capture companies within the window of 00:00 and 00:30 + if($timezone_now->gte($timezone_now->copy()->startOfDay()) && $timezone_now->lt($timezone_now->copy()->startOfDay()->addMinutes(30))) { + + $i = Invoice::query() + ->where('company_id', $company->id) + ->whereNull('deleted_at') + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_proforma', 0) + ->whereNotNull('subscription_id') + ->where('balance', '>', 0) + ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->get(); + + } + + $this->assertFalse($i); + + $this->travelTo($timezone_now->copy()->startOfDay()); + + if(now()->gte($timezone_now->copy()->startOfDay()) && now()->lt($timezone_now->copy()->startOfDay()->addMinutes(30))) { + + $i = Invoice::query() + ->where('company_id', $company->id) + ->whereNull('deleted_at') + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_proforma', 0) + ->whereNotNull('subscription_id') + ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->get(); + + } + + $this->assertEquals(1, $i->count()); + + $i = false; + + $this->travelTo($timezone_now->copy()->startOfDay()->addHours(2)); + + if($timezone_now->gte($timezone_now->copy()->startOfDay()) && $timezone_now->lt($timezone_now->copy()->startOfDay()->addMinutes(30))) { + + $i = Invoice::query() + ->where('company_id', $company->id) + ->whereNull('deleted_at') + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_proforma', 0) + ->whereNotNull('subscription_id') + ->where('balance', '>', 0) + ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->get(); + + } + + $this->assertFalse($i); + + $count = Invoice::whereNotNull('subscription_id')->count(); + + $this->assertEquals(2, $count); + + $this->travelBack(); + //////////////////////////////////////////// vienna ////////////////////////////////////////////////// + + $company = Company::find($c2->id); //vienna + + $timezone_now = now()->setTimezone($company->timezone()->name); + + $this->assertEquals('Europe/Vienna', $timezone_now->timezoneName); + + $this->travelTo($timezone_now->copy()->startOfDay()->subHour()); + + $this->travelTo($timezone_now->copy()->startOfDay()->subHour()); + + $i = false; + + //Capture companies within the window of 00:00 and 00:30 + if($timezone_now->gte($timezone_now->copy()->startOfDay()) && $timezone_now->lt($timezone_now->copy()->startOfDay()->addMinutes(30))) { + + $i = Invoice::query() + ->where('company_id', $company->id) + ->whereNull('deleted_at') + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_proforma', 0) + ->whereNotNull('subscription_id') + ->where('balance', '>', 0) + ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->get(); + + } + + $this->assertFalse($i); + + $this->travelTo($timezone_now->copy()->startOfDay()); + + if(now()->gte($timezone_now->copy()->startOfDay()) && now()->lt($timezone_now->copy()->startOfDay()->addMinutes(30))) { + + $i = Invoice::query() + ->where('company_id', $company->id) + ->whereNull('deleted_at') + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_proforma', 0) + ->whereNotNull('subscription_id') + ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->get(); + + } + + $this->assertEquals(1, $i->count()); + + $i = false; + + $this->travelTo($timezone_now->copy()->startOfDay()->addHours(2)); + + if($timezone_now->gte($timezone_now->copy()->startOfDay()) && $timezone_now->lt($timezone_now->copy()->startOfDay()->addMinutes(30))) { + + $i = Invoice::query() + ->where('company_id', $company->id) + ->whereNull('deleted_at') + ->where('is_deleted', 0) + ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) + ->where('is_proforma', 0) + ->whereNotNull('subscription_id') + ->where('balance', '>', 0) + ->whereDate('due_date', '<=', now()->setTimezone($company->timezone()->name)->addDay()->startOfDay()) + ->get(); + + } + + $this->assertFalse($i); + + + + } + public function testAssignInvoice() { $i = Invoice::factory()