Skip to content
6 changes: 3 additions & 3 deletions src/action/AbstractAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@
QuantityInterface $quantity,
CustomerInterface $customer,
DateTimeImmutable $time,
SaleInterface $sale = null,
ActionState $state = null,
ActionInterface $parent = null,
?SaleInterface $sale = null,
?ActionState $state = null,
?ActionInterface $parent = null,
float $fractionOfMonth = 0.0
) {
$this->id = $id;
Expand All @@ -89,7 +89,7 @@
* Provides unique string.
* Can be used to compare or aggregate actions.
*/
public function getUniqueString(): string

Check failure on line 92 in src/action/AbstractAction.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

PossiblyUnusedMethod

src/action/AbstractAction.php:92:21: PossiblyUnusedMethod: Cannot find any calls to method hiqdev\php\billing\action\AbstractAction::getUniqueString (see https://psalm.dev/087)
{
$parts = [
'buyer' => $this->customer->getUniqueId(),
Expand Down Expand Up @@ -157,7 +157,7 @@
return $this->time;
}

public function setTime(DateTimeImmutable $time)

Check failure on line 160 in src/action/AbstractAction.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

PossiblyUnusedMethod

src/action/AbstractAction.php:160:21: PossiblyUnusedMethod: Cannot find any calls to method hiqdev\php\billing\action\AbstractAction::setTime (see https://psalm.dev/087)
{
$this->time = $time;
}
Expand All @@ -167,7 +167,7 @@
return $this->state;
}

public function setFinished(): void

Check failure on line 170 in src/action/AbstractAction.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

PossiblyUnusedMethod

src/action/AbstractAction.php:170:21: PossiblyUnusedMethod: Cannot find any calls to method hiqdev\php\billing\action\AbstractAction::setFinished (see https://psalm.dev/087)
{
$this->state = ActionState::finished();
}
Expand All @@ -188,7 +188,7 @@
/**
* {@inheritdoc}
*/
public function hasParent()

Check failure on line 191 in src/action/AbstractAction.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

PossiblyUnusedMethod

src/action/AbstractAction.php:191:21: PossiblyUnusedMethod: Cannot find any calls to method hiqdev\php\billing\action\AbstractAction::hasParent (see https://psalm.dev/087)
{
return $this->parent !== null;
}
Expand All @@ -198,7 +198,7 @@
return $this->id !== null;
}

public function setId($id)

Check failure on line 201 in src/action/AbstractAction.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

PossiblyUnusedMethod

src/action/AbstractAction.php:201:21: PossiblyUnusedMethod: Cannot find any calls to method hiqdev\php\billing\action\AbstractAction::setId (see https://psalm.dev/087)
{
if ((string) $this->id === (string) $id) {
return;
Expand Down
45 changes: 42 additions & 3 deletions src/bill/Bill.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,29 @@ class Bill implements BillInterface
/** @var UsageInterval */
protected $usageInterval;

/** @var BillSource|null */
protected $source;

/** @var BillTxn|null */
protected $txn;

/** @var BillReversesId|null */
protected $reversesId;

public function __construct(
$id,
TypeInterface $type,
DateTimeImmutable $time,
Money $sum,
QuantityInterface $quantity,
CustomerInterface $customer,
TargetInterface $target = null,
PlanInterface $plan = null,
?TargetInterface $target = null,
?PlanInterface $plan = null,
array $charges = [],
BillState $state = null
?BillState $state = null,
?BillSource $source = null,
?BillTxn $txn = null,
?BillReversesId $reversesId = null,
) {
$this->id = $id;
$this->type = $type;
Expand All @@ -89,6 +101,9 @@ public function __construct(
$this->plan = $plan;
$this->charges = $charges;
$this->state = $state;
$this->source = $source;
$this->txn = $txn;
$this->reversesId = $reversesId;
}

/**
Expand Down Expand Up @@ -242,6 +257,16 @@ public function getComment()
return $this->comment;
}

public function getSource(): ?BillSource
{
return $this->source;
}

public function getTxn(): ?BillTxn
{
return $this->txn;
}

public function setComment(string $comment)
{
$this->comment = $comment;
Expand All @@ -256,4 +281,18 @@ public function setUsageInterval(UsageInterval $usageInterval): void
{
$this->usageInterval = $usageInterval;
}

public function getReversesId(): ?BillReversesId
{
return $this->reversesId;
}

/**
* @param BillReversesId|null $reversesId
* @return void
*/
public function setReversesId($reversesId): void
{
$this->reversesId = $reversesId;
}
Comment on lines +290 to +297
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

setReversesId parameter is untyped — add ?BillReversesId hint

The @param BillReversesId|null docblock documents the intended type, but the actual parameter declaration is untyped. This is inconsistent with the constructor signature (line 92: BillReversesId $reversesId = null) and silently accepts any value at runtime.

♻️ Proposed fix
-    public function setReversesId($reversesId): void
+    public function setReversesId(?BillReversesId $reversesId): void
     {
         $this->reversesId = $reversesId;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bill/Bill.php` around lines 290 - 297, The setter setReversesId is
missing the nullable type hint documented in the docblock; change its signature
to accept ?BillReversesId (i.e. public function setReversesId(?BillReversesId
$reversesId): void) so it matches the constructor's BillReversesId $reversesId =
null intent and prevents arbitrary values, and ensure any necessary import/use
for BillReversesId is present and the docblock stays consistent.

}
6 changes: 6 additions & 0 deletions src/bill/BillInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,10 @@ public function getCharges();
public function getUsageInterval(): UsageInterval;

public function setUsageInterval(UsageInterval $usageInterval): void;

public function getSource(): ?BillSource;

public function getTxn(): ?BillTxn;

public function getReversesId(): ?BillReversesId;
}
2 changes: 1 addition & 1 deletion src/bill/BillRequisite.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class BillRequisite

protected ?string $name = null;

public function __construct($id = null, string $name = null)
public function __construct($id = null, ?string $name = null)
{
$this->id = $id;
$this->name = $name;
Expand Down
26 changes: 26 additions & 0 deletions src/bill/BillReversesId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace hiqdev\php\billing\bill;

class BillReversesId
{
/** @var int|null */
protected $id;

public function __construct($id = null)
{
$this->id = $id;
}

public function getId(): int
{
return $this->id;
Comment on lines +9 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

getId(): int will throw TypeError when $id is null

The property is documented as int|null and the constructor defaults $id to null, so any instance created via new BillReversesId() or new BillReversesId(null) will throw a TypeError when getId() is called under declare(strict_types=1). The docblock and return type are contradictory.

Either make the return type nullable to match the property, or restrict the constructor so a null instance cannot be created:

🐛 Option A — fix the return type to match the nullable property
-    public function getId(): int
+    public function getId(): ?int
     {
         return $this->id;
     }
🐛 Option B — restrict construction to the typed factory only
-    public function __construct($id = null)
+    private function __construct(int $id)
     {
         $this->id = $id;
     }

     public function getId(): int
     {
         return $this->id;
     }

With a private constructor, callers are forced through fromInt(int $id) and a null state becomes impossible.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** @var int|null */
protected $id;
public function __construct($id = null)
{
$this->id = $id;
}
public function getId(): int
{
return $this->id;
/** `@var` int|null */
protected $id;
public function __construct($id = null)
{
$this->id = $id;
}
public function getId(): ?int
{
return $this->id;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bill/BillReversesId.php` around lines 9 - 19, The getId(): int return
type is incompatible with the protected $id (int|null) and the public
__construct($id = null); either make the return nullable by changing getId():
?int to reflect the property, or prevent null construction by making __construct
private and add a static factory like fromInt(int $id): self that sets
$this->id; locate the class BillReversesId, the $id property, the __construct
and getId methods and apply one of these two fixes consistently so types no
longer conflict.

}

public static function fromInt(int $id): self
{
return new self($id);
}
}
29 changes: 29 additions & 0 deletions src/bill/BillSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace hiqdev\php\billing\bill;

class BillSource
{
/** @var int|string|null */
protected $id;

protected ?string $name = null;

public function __construct($id = null, ?string $name = null)
{
$this->id = $id;
$this->name = $name;
}

public function getId()
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}
}
49 changes: 49 additions & 0 deletions src/bill/BillTxn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace hiqdev\php\billing\bill;

/**
* BillTxn
*
* Represents an immutable external payment transaction identifier.
*
* A TransactionId uniquely identifies a financial transaction
* as defined by an external payment or accounting system
* (e.g. Business Central, merchant gateways, banks).
*
* This value:
* - Is created by an external system, never generated internally
* - Is stable across retries, webhooks, and re-imports
* - Is used for idempotency and reconciliation
* - Has meaning outside of the database and application boundaries
*
* Uniqueness is guaranteed only within the scope of a source system
* (see Source / external system).
*
* Typical examples:
* - UUIDs
* - Gateway transaction references
* - Accounting document numbers
*/
class BillTxn
{
/** @var int|string|null */
protected $value;

public function __construct($txn = null)
{
$this->value = $txn;
}

public function getValue()
{
return $this->value;
}

public static function fromString(string $txn): self
{
return new self($txn);
}
}
2 changes: 1 addition & 1 deletion src/charge/Charge.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function __construct(
?PriceInterface $price,
QuantityInterface $usage,
Money $sum,
BillInterface $bill = null
?BillInterface $bill = null
) {
$this->id = $id;
$this->type = $type;
Expand Down
2 changes: 1 addition & 1 deletion src/charge/modifiers/Installment.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function buildPrice(Money $sum)
$target = $this->getTarget();
$prepaid = Quantity::create('items', 0);

return new SinglePrice(null, $type, $target, null, $prepaid, $sum);
return new SinglePrice(null, $type, $target, $prepaid, $sum);
}

public function getType()
Expand Down
2 changes: 1 addition & 1 deletion src/customer/Customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class Customer implements CustomerInterface
*/
protected $sellers = [];

public function __construct($id, $login, CustomerInterface $seller = null)
public function __construct($id, $login, ?CustomerInterface $seller = null)
{
$this->id = $id;
$this->login = $login;
Expand Down
4 changes: 2 additions & 2 deletions src/plan/Plan.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ class Plan implements PlanInterface
public function __construct(
$id,
$name,
CustomerInterface $seller = null,
?CustomerInterface $seller = null,
$prices = [],
TypeInterface $type = null,
?TypeInterface $type = null,
$parent_id = null
) {
$this->id = $id;
Expand Down
2 changes: 1 addition & 1 deletion src/price/AbstractPrice.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function __construct(
$id,
TypeInterface $type,
TargetInterface $target,
PlanInterface $plan = null
?PlanInterface $plan = null
) {
$this->id = $id;
$this->type = $type;
Expand Down
2 changes: 1 addition & 1 deletion src/price/PriceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function createRatePrice(PriceCreationDto $dto)

public function createSinglePrice(PriceCreationDto $dto)
{
return new SinglePrice($dto->id, $dto->type, $dto->target, $dto->plan, $dto->prepaid, $dto->price);
return new SinglePrice($dto->id, $dto->type, $dto->target, $dto->prepaid, $dto->price, $dto->plan);
}

public function createProgressivePrice(PriceCreationDto $dto): ProgressivePrice
Expand Down
4 changes: 2 additions & 2 deletions src/price/SinglePrice.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public function __construct(
$id,
TypeInterface $type,
TargetInterface $target,
PlanInterface $plan = null,
QuantityInterface $prepaid,
Money $price
Money $price,
?PlanInterface $plan = null,
) {
parent::__construct($id, $type, $target, $plan);
$this->prepaid = $prepaid;
Expand Down
2 changes: 1 addition & 1 deletion src/product/price/PriceTypeDefinitionCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function __construct(
*/
private readonly TariffTypeDefinitionInterface $parent,
private readonly PriceTypeDefinitionFactoryInterface $factory,
PriceTypeDefinitionCollectionInterface $collectionInstance = null,
?PriceTypeDefinitionCollectionInterface $collectionInstance = null,
) {
$this->storage = new PriceTypeStorage();
$this->collectionInstance = $collectionInstance ?? $this;
Expand Down
2 changes: 1 addition & 1 deletion tests/behat/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public function priceIs($target, $type, $sum, $currency, $unit, $quantity = 0)
$target = new Target(Target::ANY, $target);
$quantity = Quantity::create($unit, $quantity);
$sum = $this->moneyParser->parse($sum, new Currency($currency));
$this->setPrice(new SinglePrice(null, $type, $target, null, $quantity, $sum));
$this->setPrice(new SinglePrice(null, $type, $target, $quantity, $sum));
}

protected array $progressivePrice = [];
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/action/ActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected function setUp(): void
$this->target = new Target(2, 'server');
$this->prepaid = Quantity::gigabyte(1);
$this->money = Money::USD(10000);
$this->price = new SinglePrice(5, $this->type, $this->target, null, $this->prepaid, $this->money);
$this->price = new SinglePrice(5, $this->type, $this->target, $this->prepaid, $this->money);
$this->customer = new Customer(2, 'client');
$this->time = new DateTimeImmutable('now');
$this->generalizer = new Generalizer();
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/charge/modifiers/InstallmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected function setUp(): void
{
parent::setUp();
$this->type = Type::anyId('monthly,installment');
$this->price = new SinglePrice(5, $this->type, $this->target, null, $this->prepaid, $this->money);
$this->price = new SinglePrice(5, $this->type, $this->target, $this->prepaid, $this->money);
}

protected function buildInstallment($term)
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/charge/modifiers/OnceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private function createType(string $name): TypeInterface

private function createPrice(TypeInterface $type): PriceInterface
{
return new SinglePrice(5, $type, $this->target, null, $this->prepaid, $this->money);
return new SinglePrice(5, $type, $this->target, $this->prepaid, $this->money);
}

#[\PHPUnit\Framework\Attributes\DataProvider('periodCreationProvider')]
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/price/SinglePriceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected function setUp(): void
$this->type = new Type(null, 'server_traf');
$this->quantity = Quantity::gigabyte(10);
$this->money = Money::USD(15);
$this->price = new SinglePrice(null, $this->type, $this->target, null, $this->quantity, $this->money);
$this->price = new SinglePrice(null, $this->type, $this->target, $this->quantity, $this->money);
}

protected function tearDown(): void
Expand Down
Loading