laravel-cashier-mollie
laravel-cashier-mollie copied to clipboard
swapAndInvoice giving unexpected result
I am using $subscription->swapAndInvoice of the Mollie Cashier repository. Usually it should refund the unused days and invoice for the full new price. Which normally works. However with the latest swapAndInvoice my customer suddenly doesn't get their old price refunded but instead the cycle gets lengthened to 30-09-2024. So now his cycle runs from 14-08-2024 to 30-09-2024. This brings two issues:
Issue 1. He didn't pay the new price for 14-08-2024 to 30-08-2024 but did instantly get the new features; Issue 2. Maybe he didn't want the extend from 30-08-2024 to 30-09-2024.
This is in the Cashier\Subscription model:
<?php
namespace App\Models\Cashier;
use Carbon\Carbon;
use Closure;
use Illuminate\Support\Facades\DB;
use Laravel\Cashier\Cashier;
use Laravel\Cashier\Coupon\AppliedCoupon;
use Laravel\Cashier\Order\OrderItem;
use Laravel\Cashier\Order\OrderItemCollection;
use Laravel\Cashier\Refunds\RefundItemCollection;
use Laravel\Cashier\Subscription as CashierSubscription;
use LogicException;
use Money\Currency;
use Money\Money;
class Subscription extends CashierSubscription
{
/**
* The maximum amount that can be reimbursed, ranging from zero to a positive Money amount.
*
* @return Money
*/
protected function reimbursableAmount(): Money
{
$zeroAmount = new Money('0.00', new Currency($this->currency));
// Determine base amount eligible to reimburse
$latestProcessedOrderItem = $this->latestProcessedOrderItem();
if (!$latestProcessedOrderItem) {
return $zeroAmount;
}
$reimbursableAmount = $latestProcessedOrderItem->getTotal()
->subtract($latestProcessedOrderItem->getTax()); // tax calculated elsewhere
// Subtract any refunds
/** @var RefundItemCollection $refundItems */
$refundItems = Cashier::$refundItemModel::where('original_order_item_id', $latestProcessedOrderItem->id)->get();
if ($refundItems->isNotEmpty()) {
$reimbursableAmount = $reimbursableAmount->subtract($refundItems->getTotal());
}
// Subtract any applied coupons
$order = $latestProcessedOrderItem->order;
$orderId = $order->id;
$appliedCoupons = $this->appliedCoupons()->with('orderItems')->get();
$appliedCouponOrderItems = $appliedCoupons->reduce(function (OrderItemCollection $carry, AppliedCoupon $coupon) use ($orderId) {
$items = $coupon->orderItems->filter(function (OrderItem $item) use ($orderId) {
return $item->order_id === $orderId;
});
return $carry->concat($items->toArray());
}, new OrderItemCollection);
if ($appliedCouponOrderItems->isNotEmpty()) {
$discountTotal = $this->getTotalOfOrderItems($appliedCouponOrderItems);
$reimbursableAmount = $reimbursableAmount->subtract($discountTotal->absolute());
}
// Guard against a negative value
if ($reimbursableAmount->isNegative()) {
return $zeroAmount;
}
return $reimbursableAmount;
}
/**
* @return \Money\Money
*
* @throws \LogicException
*/
public function getTotalOfOrderItems($appliedCouponOrderItems): Money
{
if (count($appliedCouponOrderItems->currencies()) > 1) {
throw new LogicException('Calculating the total requires items to be of the same currency.');
}
return new Money($appliedCouponOrderItems->sum('unit_price'), new Currency($appliedCouponOrderItems->currency()));
}
}
Plan Model:
<?php
namespace App\Models;
use Laravel\Cashier\Plan\Plan as CashierPlan;
use Illuminate\Database\Eloquent\Model;
use Laravel\Cashier\Order\OrderItemPreprocessorCollection as Preprocessors;
/**
* @property mixed $name
* @property mixed $amount
* @property mixed $interval
* @property mixed $description
* @property mixed $first_payment_method
* @property mixed $first_payment_amount
* @property mixed $first_payment_description
* @property mixed $first_payment_redirect_url
* @property mixed $first_payment_webhook_url
* @property mixed $order_item_preprocessors
* @method static where(string $string, string $name)
* @method static whereIn(string $string, mixed[] $getSelected)
*/
class Plan extends Model
{
public $fillable = [
'name',
'amount',
'interval',
'description',
'first_payment_method',
'first_payment_amount',
'first_payment_description',
'first_payment_redirect_url',
'first_payment_webhook_url',
'order_item_preprocessors',
];
/**
* Builds a Cashier plan from the current model.
*
* @returns CashierPlan
*/
public function buildCashierPlan(): CashierPlan
{
$plan = new CashierPlan($this->name);
return $plan->setAmount(mollie_array_to_money($this->amount))
->setInterval($this->interval)
->setDescription($this->description)
->setFirstPaymentMethod($this->first_payment_method)
->setFirstPaymentAmount(mollie_array_to_money($this->first_payment_amount))
->setFirstPaymentDescription($this->first_payment_description)
->setFirstPaymentRedirectUrl($this->first_payment_redirect_url)
->setFirstPaymentWebhookUrl($this->first_payment_webhook_url)
->setOrderItemPreprocessors(Preprocessors::fromArray(config('cashier_plans.defaults.order_item_preprocessors')));
}
}
PlanRepository:
<?php
namespace App\Repositories;
use App\Models\Plan;
use Laravel\Cashier\Exceptions\PlanNotFoundException;
use Laravel\Cashier\Plan\Contracts\PlanRepository as PlanRepositoryContract;
class PlanRepository implements PlanRepositoryContract
{
public static function find(string $name)
{
$plan = Plan::where('name', $name)->first();
if (is_null($plan)) {
return null;
}
if ($plan['first_payment_method'] == '[]') $plan['first_payment_method'] = [];
$plan['amount'] = json_decode($plan['amount'], true);
$plan['first_payment_amount'] = json_decode($plan['first_payment_amount'], true);
return $plan->buildCashierPlan();
}
/**
* @throws PlanNotFoundException
*/
public static function findOrFail(string $name)
{
if (($result = self::find($name)) === null) {
throw new PlanNotFoundException;
}
return $result;
}
}
This is the expected result:
But it simply gives a new invoice with only the new price and adds this to the database cycle: