Skip to content
123 changes: 123 additions & 0 deletions src/Pay/Credit/Credit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace Utopia\Pay\Credit;

class Credit
{
public const STATUS_ACTIVE = 'active';

public const STATUS_APPLIED = 'applied';

public const STATUS_EXPIRED = 'expired';

/**
* @param string $id
* @param float $credits
* @param int $creditsUsed
* @param string $status
*/
public function __construct(private string $id, private float $credits, private float $creditsUsed = 0, private string $status = self::STATUS_ACTIVE)
{
}

public function getStatus(): string
{
return $this->status;
}

public function markAsApplied(): static
{
$this->status = self::STATUS_APPLIED;

return $this;
}

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

public function setId(string $id): static
{
$this->id = $id;

return $this;
}

public function getCredits(): float
{
return $this->credits;
}

public function setCredits(float $credits): static
{
$this->credits = $credits;

return $this;
}

public function getCreditsUsed(): float
{
return $this->creditsUsed;
}

public function setCreditsUsed(float $creditsUsed): static
{
$this->creditsUsed = $creditsUsed;

return $this;
}

public function hasAvailableCredits(): bool
{
return $this->credits > 0;
}

public function useCredits(float $amount): float
{
if ($amount <= 0) {
return 0;
}

$creditsToUse = min($amount, $this->credits);
$this->credits -= $creditsToUse;
$this->creditsUsed += $creditsToUse;

if ($this->credits === 0) {
$this->status = self::STATUS_APPLIED;
}

return $creditsToUse;
}

public function setStatus($status): static
{
$this->status = $status;

return $this;
}

public function isFullyUsed(): bool
{
return $this->credits === 0 || $this->status === self::STATUS_APPLIED;
}

public static function fromArray(array $data): self
{
return new self($data['id'] ?? $data['$id'] ?? '',
$data['credits'] ?? 0.0,
$data['creditsUsed'] ?? 0.0,
$data['status'] ?? self::STATUS_ACTIVE
);
}

public function toArray(): array
{
return [
'id' => $this->id,
'credits' => $this->credits,
'creditsUsed' => $this->creditsUsed,
'status' => $this->status,
];
}
}
121 changes: 121 additions & 0 deletions src/Pay/Discount/Discount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

namespace Utopia\Pay\Discount;

class Discount
{
public const TYPE_FIXED = 'fixed'; // Fixed amount discount

public const TYPE_PERCENTAGE = 'percentage'; // Percentage discount

/**
* @param string $id
* @param float $value
* @param float $amount
* @param string $description
* @param string $type
*/
public function __construct(private string $id, private float $value, private float $amount, private string $description = '', private string $type = self::TYPE_FIXED)
{
}

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

public function setId(string $id): static
{
$this->id = $id;

return $this;
}

public function getAmount(): float
{
return $this->amount;
}

public function setAmount(float $amount): static
{
$this->amount = $amount;

return $this;
}

public function getDescription(): string
{
return $this->description;
}

public function setDescription(string $description): static
{
$this->description = $description;

return $this;
}

public function getType(): string
{
return $this->type;
}

public function setType(string $type): static
{
$this->type = $type;

return $this;
}

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

public function setValue(float $value): static
{
$this->value = $value;

return $this;
}
Comment on lines +70 to +80
Copy link
Contributor

Choose a reason for hiding this comment

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

value and amount are a bit confusing. At the very least, this needs some comments to explain what they are.


public function calculateDiscount(float $amount): float
Copy link
Contributor

Choose a reason for hiding this comment

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

comments to explain this method and what $amount is would be helpful

{
if ($this->type === self::TYPE_FIXED) {
return min($this->amount, $amount);
} elseif ($this->type === self::TYPE_PERCENTAGE) {
return ($this->value / 100) * $amount;
}
Comment on lines +84 to +88
Copy link
Contributor

Choose a reason for hiding this comment

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

What if someone passes a negative value for amount?


return 0;
}
Comment on lines +82 to +91
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fixed discount calculation uses $this->amount instead of $this->value

The calculation for TYPE_FIXED discounts uses $this->amount (Line 85) while TYPE_PERCENTAGE uses $this->value (Line 87). This inconsistency is confusing and error-prone.

For fixed discounts, both value and amount are typically set to the same value (as seen in the tests), but conceptually value should be the discount value to apply, while amount might serve a different purpose (e.g., tracking total discount given).

Consider using $this->value consistently for both discount types:

 public function calculateDiscount(float $amount): float
 {
     if ($this->type === self::TYPE_FIXED) {
-        return min($this->amount, $amount);
+        return min($this->value, $amount);
     } elseif ($this->type === self::TYPE_PERCENTAGE) {
         return ($this->value / 100) * $amount;
     }
 
     return 0;
 }
📝 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
public function calculateDiscount(float $amount): float
{
if ($this->type === self::TYPE_FIXED) {
return min($this->amount, $amount);
} elseif ($this->type === self::TYPE_PERCENTAGE) {
return ($this->value / 100) * $amount;
}
return 0;
}
public function calculateDiscount(float $amount): float
{
if ($this->type === self::TYPE_FIXED) {
return min($this->value, $amount);
} elseif ($this->type === self::TYPE_PERCENTAGE) {
return ($this->value / 100) * $amount;
}
return 0;
}
🤖 Prompt for AI Agents
In src/Pay/Discount/Discount.php around lines 82 to 91, the TYPE_FIXED branch
uses $this->amount instead of $this->value which is inconsistent with the
TYPE_PERCENTAGE branch; change the fixed-discount calculation to use
$this->value (e.g., return min($this->value, $amount)) so both discount types
use the intended discount value property while preserving the cap at the
transaction amount.


public function isValid(): bool
{
return $this->amount > 0 && $this->type === self::TYPE_FIXED || $this->value > 0 && $this->type === self::TYPE_PERCENTAGE;
}

public function toArray()
{
return [
'id' => $this->id,
'amount' => $this->amount,
'value' => $this->value,
'description' => $this->description,
'type' => $this->type,
];
}

public static function fromArray($data)
{
$discount = new self(
$data['id'] ?? $data['$id'] ?? '',
$data['value'] ?? 0,
$data['amount'] ?? 0,
$data['description'] ?? '',
$data['type'] ?? self::TYPE_FIXED,
);

return $discount;
}
}
Loading