Skip to content

Commit

Permalink
Added option to configure how long after purchase (or end of the month)
Browse files Browse the repository at this point in the history
should be generation of payment's invoice allowed

remp/crm#2799
- **BREAKING**: Changed `InvoicesRepository::paymentInInvoiceablePeriod` from static to instance method
- Until now invoice could be generated 15 days after purchase date.
This behavior is kept (no breaking change) by presetting this config to "15 days after purchase date"
- **IMPORTANT**: This config is restricted: If number of days is bigger than 15,
system requires you to enable config "Generate an invoice number for every paid payment" 
- Added support for select boxes to application config forms. remp/crm#2799
  • Loading branch information
zoldia committed May 29, 2023
1 parent 5b6ef3b commit c30aea7
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/Components/InvoiceButton/InvoiceButton.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function render($payment)
{
$this->template->payment = $payment;
$this->template->paymentInvoicable = $this->invoicesRepository->isPaymentInvoiceable($payment);
$this->template->paidButNotInvoiceableAnymore = $payment->paid_at !== null && !InvoicesRepository::paymentInInvoiceablePeriod($payment, new DateTime());
$this->template->paidButNotInvoiceableAnymore = $payment->paid_at !== null && !$this->invoicesRepository->paymentInInvoiceablePeriod($payment, new DateTime());
$this->template->admin = $this->admin;
$this->template->setFile(__DIR__ . '/' . $this->templateName);
$this->template->render();
Expand Down
41 changes: 41 additions & 0 deletions src/DataProviders/ConfigFormDataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Crm\InvoicesModule\DataProvider;

use Contributte\Translation\Translator;
use Crm\AdminModule\DataProvider\ConfigFormDataProviderInterface;
use Crm\ApplicationModule\DataProvider\DataProviderException;
use Crm\InvoicesModule\Repository\InvoicesRepository;
use Nette\Application\UI\Form;

class ConfigFormDataProvider implements ConfigFormDataProviderInterface
{
public const GENERATE_INVOICE_NUMBER_LIMIT = 15;

public function __construct(private Translator $translator)
{
}

public function provide(array $params): Form
{
if (!isset($params['form'])) {
throw new DataProviderException('missing [form] within data provider params');
}

/** @var Form $form */
$form = $params['form'];
if ($form->getComponent('generate_invoice_number_for_paid_payment', false)) {
$generateInvoiceLimitFromDays = $form->getComponent(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_DAYS);

$form->getComponent('generate_invoice_number_for_paid_payment')
->addConditionOn($generateInvoiceLimitFromDays, Form::Min, self::GENERATE_INVOICE_NUMBER_LIMIT + 1)
->setRequired($this->translator->translate(
'invoices.config.generate_invoice_number_for_paid_payment.required_because_of_invoice_limit_from_days',
[
'days' => self::GENERATE_INVOICE_NUMBER_LIMIT
]
));
}
return $form;
}
}
5 changes: 5 additions & 0 deletions src/InvoicesModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ public function registerDataProviders(DataProviderManager $dataProviderManager)
'users.dataprovider.user_form',
$this->getInstance(\Crm\InvoicesModule\DataProvider\UserFormDataProvider::class)
);

$dataProviderManager->registerDataProvider(
'admin.dataprovider.config_form',
$this->getInstance(\Crm\InvoicesModule\DataProvider\ConfigFormDataProvider::class)
);
}

public function registerUserData(UserDataRegistrator $dataRegistrator)
Expand Down
23 changes: 19 additions & 4 deletions src/Models/Repository/InvoicesRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

class InvoicesRepository extends Repository
{
public const PAYMENT_INVOICEABLE_PERIOD_DAYS = 15;
public const GENERATE_INVOICE_LIMIT_FROM = 'generate_invoice_limit_from';
public const GENERATE_INVOICE_LIMIT_FROM_DAYS = 'generate_invoice_limit_from_days';
public const GENERATE_INVOICE_LIMIT_FROM_END_OF_THE_MONTH = 'limit_from_end_of_month';
public const GENERATE_INVOICE_LIMIT_FROM_PAYMENT = 'limit_from_payment';

protected $tableName = 'invoices';

Expand Down Expand Up @@ -195,7 +198,7 @@ final public function isInvoiceNumberGeneratable(ActiveRow $payment): bool
return false;
}

if (!self::paymentInInvoiceablePeriod($payment, new DateTime())) {
if (!$this->paymentInInvoiceablePeriod($payment, new DateTime())) {
return false;
}

Expand All @@ -207,9 +210,21 @@ final public function isInvoiceNumberGeneratable(ActiveRow $payment): bool
*
* Warning: This is not full validation if payment is invoiceable. Use `isPaymentInvoiceable()`.
*/
final public static function paymentInInvoiceablePeriod(ActiveRow $payment, DateTime $now): bool
final public function paymentInInvoiceablePeriod(ActiveRow $payment, DateTime $now): bool
{
$maxInvoiceableDate = $payment->paid_at->modifyClone('+' . self::PAYMENT_INVOICEABLE_PERIOD_DAYS . 'days 23:59:59');
$limitFrom = $this->applicationConfig->get(self::GENERATE_INVOICE_LIMIT_FROM);
$limitDays = $this->applicationConfig->get(self::GENERATE_INVOICE_LIMIT_FROM_DAYS);

/** @var DateTime $paidAt */
$paidAt = $payment->paid_at;
if ($limitFrom === self::GENERATE_INVOICE_LIMIT_FROM_END_OF_THE_MONTH) {
$maxInvoiceableDate = $paidAt->modifyClone('last day of this month')->modify('+' . $limitDays . 'days 23:59:59');
} elseif ($limitFrom === self::GENERATE_INVOICE_LIMIT_FROM_PAYMENT) {
$maxInvoiceableDate = $paidAt->modifyClone('+' . $limitDays . 'days 23:59:59');
} else {
$generateInvoiceLimitFromKey = self::GENERATE_INVOICE_LIMIT_FROM;
throw new \Exception("Invalid application configuration option for config: '{$generateInvoiceLimitFromKey}', value: '{$limitFrom}'");
}
return $maxInvoiceableDate >= $now;
}
}
27 changes: 27 additions & 0 deletions src/Seeders/ConfigsSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Crm\ApplicationModule\Config\Repository\ConfigsRepository;
use Crm\ApplicationModule\Seeders\ConfigsTrait;
use Crm\ApplicationModule\Seeders\ISeeder;
use Crm\InvoicesModule\Repository\InvoicesRepository;
use Symfony\Component\Console\Output\OutputInterface;

class ConfigsSeeder implements ISeeder
Expand Down Expand Up @@ -200,5 +201,31 @@ public function seed(OutputInterface $output)
false, // keeping compatible with previous state of invoice generation
1400
);

$this->addConfig(
$output,
$category,
InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM,
ApplicationConfig::TYPE_SELECT,
'invoices.config.generate_invoice_limit_from.name',
'invoices.config.generate_invoice_limit_from.description',
'limit_from_payment',
1500,
[
InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_PAYMENT => 'invoices.config.generate_invoice_limit_from.options.limit_from_payment',
InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_END_OF_THE_MONTH => 'invoices.config.generate_invoice_limit_from.options.limit_from_end_of_the_month',
]
);

$this->addConfig(
$output,
$category,
InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_DAYS,
ApplicationConfig::TYPE_INT,
'invoices.config.generate_invoice_limit_from_days.name',
'invoices.config.generate_invoice_limit_from_days.description',
15, // default set to 15 days from payment paid_at to avoid breaking change
1505
);
}
}
84 changes: 84 additions & 0 deletions src/Tests/Repository/InvoicesRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,90 @@ public function testMissingAddressNoInvoice()
$this->assertEquals(0, $this->invoiceItemsRepository->totalCount());
}

public function testPaymentIsInInvoiceablePeriodFromPayment()
{
$limitFromConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM);
$this->configsRepository->update($limitFromConfig, [
'value' => InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_PAYMENT
]);

$limitFromDaysConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_DAYS);
$this->configsRepository->update($limitFromDaysConfig, [
'value' => 15
]);

$user = $this->getUser();
$now = new DateTime();
$in13Days = DateTime::from('+13 days 23:59:59');
$payment = $this->addPayment($user, $now, $now, $this->getSubscriptionType());

$isPaymentInvoiceable = $this->invoicesRepository->paymentInInvoiceablePeriod($payment, $in13Days);
$this->assertTrue($isPaymentInvoiceable);
}

public function testPaymentIsNotInInvoiceablePeriodFromPayment()
{
$limitFromConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM);
$this->configsRepository->update($limitFromConfig, [
'value' => InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_PAYMENT
]);

$limitFromDaysConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_DAYS);
$this->configsRepository->update($limitFromDaysConfig, [
'value' => 15
]);

$user = $this->getUser();
$now = new DateTime();
$in16Days = DateTime::from('+16 days 23:59:59');
$payment = $this->addPayment($user, $now, $now, $this->getSubscriptionType());

$isPaymentInvoiceable = $this->invoicesRepository->paymentInInvoiceablePeriod($payment, $in16Days);
$this->assertFalse($isPaymentInvoiceable);
}

public function testPaymentIsInInvoiceablePeriodFromEndOfTheMonth()
{
$limitFromConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM);
$this->configsRepository->update($limitFromConfig, [
'value' => InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_END_OF_THE_MONTH
]);

$limitFromDaysConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_DAYS);
$this->configsRepository->update($limitFromDaysConfig, [
'value' => 15
]);

$user = $this->getUser();
$now = new DateTime();
$in13DaysFromEndOfTheMonth = $now->modifyClone('last day of this month')->modify('+13 days');
$payment = $this->addPayment($user, $now, $now, $this->getSubscriptionType());

$isPaymentInvoiceable = $this->invoicesRepository->paymentInInvoiceablePeriod($payment, $in13DaysFromEndOfTheMonth);
$this->assertTrue($isPaymentInvoiceable);
}

public function testPaymentIsNotInInvoiceablePeriodFromEndOfTheMonth()
{
$limitFromConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM);
$this->configsRepository->update($limitFromConfig, [
'value' => InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_END_OF_THE_MONTH
]);

$limitFromDaysConfig = $this->configsRepository->loadByName(InvoicesRepository::GENERATE_INVOICE_LIMIT_FROM_DAYS);
$this->configsRepository->update($limitFromDaysConfig, [
'value' => 15
]);

$user = $this->getUser();
$now = new DateTime();
$in16DaysFromEndOfTheMonth = $now->modifyClone('last day of this month')->modify('+16 days');
$payment = $this->addPayment($user, $now, $now, $this->getSubscriptionType());

$isPaymentInvoiceable = $this->invoicesRepository->paymentInInvoiceablePeriod($payment, $in16DaysFromEndOfTheMonth);
$this->assertFalse($isPaymentInvoiceable);
}

/* *******************************************************************
* Helper functions
* ***************************************************************** */
Expand Down
1 change: 1 addition & 0 deletions src/config/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ services:
- Crm\InvoicesModule\Components\PaymentSuccessInvoiceWidget

- Crm\InvoicesModule\DataProvider\UserFormDataProvider
- Crm\InvoicesModule\DataProvider\ConfigFormDataProvider
- Crm\InvoicesModule\Events\AddressChangedHandler
- Crm\InvoicesModule\Events\AddressRemovedHandler
- Crm\InvoicesModule\Events\NewAddressHandler
Expand Down
13 changes: 13 additions & 0 deletions src/lang/invoices.cs_CZ.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,16 @@ config:
name: 'Vygenerovat a odeslat fakturu jako přílohu k notifikaci po platbě'
generate_invoice_after_payment:
name: 'Vygenerovat fakturu ihned po platbě'
generate_invoice_number_for_paid_payment:
name: 'Vygenerovat číslo faktury pro každou zaplacenou platbu'
description: "Číslo faktury bude vygenerováno pro každou zaplacenou platbu ignorující chybějící adresu, nastavení uživatele (nefakturovat) a `invoiceable` nastavení platby. Faktura bude vygenerována (a ke stažení) ihned jakmile uživatel přidá/opraví fakturační adresu nebo povolí fakturování pro svůj účet. Generování čísla faktury bez vygenerování faktury pomůže dodržet číselník faktur a usnadní přegenerování a stažení faktury po skončení fakturační doby (ale před účetním uzavřením měsíce)."
required_because_of_invoice_limit_from_days: "Toto pole je povinné, pokud je nastaven \"Počet dní na vygenerování faktury\" větší než %days%."
generate_invoice_limit_from:
name: Limit vygenerování faktury počítat od
description: Určuje od jakého bodu se počítá limit na vygenerování faktury.
options:
limit_from_end_of_the_month: Posledního dne v měsíci
limit_from_payment: Data platby
generate_invoice_limit_from_days:
name: Počet dní na vygenerování faktury
description: 'Limit počtu dní do kterého systém povolí vygenerování faktury (závisí na: "Limit vygenerování faktury počítat od")'
10 changes: 10 additions & 0 deletions src/lang/invoices.en_US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,13 @@ config:
generate_invoice_number_for_paid_payment:
name: 'Generate an invoice number for every paid payment'
description: "An invoice number will be generated for each paid payment, ignoring missing user address, user settings (do not invoice) and `invoiceable` flag on payment. The invoice will be generated (and downloadable) as soon as the user adds / corrects the billing address or enables invoicing for their account. Generating an invoice number without generating an invoice will help to maintain the invoice number sequence and make it easier to generate and download the invoice after the end of the invoicing period (but before the accounting close of the month)."
required_because_of_invoice_limit_from_days: "This field is mandatory if \"Invoice generation - time restriction (in days)\" is set to more than %days% days."
generate_invoice_limit_from:
name: Invoice generation - time restriction related to
description: "It determines from which point the limit of days for invoice generation is calculated (eg. 15 days after purchase or 15 days after end of month)."
options:
limit_from_end_of_the_month: On the last day of the month
limit_from_payment: Date of payment
generate_invoice_limit_from_days:
name: Invoice generation - time restriction (in days)
description: 'Number of days after which system disables the generation of an invoice (depends on: "Invoice generation - time restriction related to")'
10 changes: 10 additions & 0 deletions src/lang/invoices.sk_SK.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,13 @@ config:
generate_invoice_number_for_paid_payment:
name: 'Vygenerovať číslo faktúry pre každú zaplatenú platbu'
description: "Číslo faktúry bude vygenerovaná pre každú zaplatenú platbu ignorujúc chýbajúcu adresu, nastavenia užívateľa (nefaktúrovať) a `invoiceable` nastavenie platby. Faktúra bude vygenerovaná (a stiahnuteľná) ihneď ako užívateľ pridá / opraví fakturačnú adresu alebo povolí faktúrovanie pre svoj účet. Generovanie čísla faktúry bez vygenerovania faktúry pomôže dodržať číselník faktúr a uľahčí pregenerovanie a stiahnutie faktúry po skončení faktúračnej doby (ale pred účtovným uzavretím mesiaca)."
required_because_of_invoice_limit_from_days: "Toto pole je povinné ak je nastavený \"Počet dní na vygenerovanie faktúry\" vačší ako %days%."
generate_invoice_limit_from:
name: Limit vygenerovania faktúry počítať od
description: Určuje od akého bodu sa počíta limit na vygenerovanie faktúry.
options:
limit_from_end_of_the_month: Posledného dňa v mesiaci
limit_from_payment: Dátumu platby
generate_invoice_limit_from_days:
name: Počet dní na vygenerovanie faktúry
description: 'Limit počtu dní do ktorého systém povolí vygenerovanie faktúry (závisí na: "Limit vygenerovania faktúry počítať od")'

0 comments on commit c30aea7

Please sign in to comment.