diff --git a/app/Console/Commands/GenerateBillingCycles.php b/app/Console/Commands/GenerateBillingCycles.php
new file mode 100644
index 00000000..01082145
--- /dev/null
+++ b/app/Console/Commands/GenerateBillingCycles.php
@@ -0,0 +1,51 @@
+argument('teamId');
+ $date = $this->argument('date');
+
+ $monthsWithTransactions = $this->getFirstTransaction($teamId);
+ return $service->generateBillingCycles($teamId, $date ?? $monthsWithTransactions->date);
+ }
+
+ private function getFirstTransaction(int $teamId) {
+ return DB::table('transaction_lines')
+ ->where([
+ "team_id" => $teamId
+ ])
+ ->selectRaw("date_format(transaction_lines.date, '%Y-%m') AS date")
+ ->groupBy(DB::raw("date_format(transaction_lines.date, '%Y-%m')"))
+ ->orderBy('date')
+ ->first();
+ }
+}
diff --git a/app/Domains/AppCore/Policies/FinanceAccountPolicy.php b/app/Domains/AppCore/Policies/FinanceAccountPolicy.php
index 6e6b0d2c..60fe7eca 100644
--- a/app/Domains/AppCore/Policies/FinanceAccountPolicy.php
+++ b/app/Domains/AppCore/Policies/FinanceAccountPolicy.php
@@ -31,6 +31,6 @@ public function create(User $user)
public function delete(User $user, Account $account)
{
- return $user->id == $account->user_id;
+ return $user->current_team_id == $account->team_id;
}
}
diff --git a/app/Domains/Budget/Services/BudgetRolloverService.php b/app/Domains/Budget/Services/BudgetRolloverService.php
index 03d5697a..1a968c65 100644
--- a/app/Domains/Budget/Services/BudgetRolloverService.php
+++ b/app/Domains/Budget/Services/BudgetRolloverService.php
@@ -9,7 +9,6 @@
use Insane\Journal\Models\Core\Account;
use Insane\Journal\Models\Core\Category;
use App\Domains\Budget\Models\BudgetMonth;
-use App\Domains\Budget\Data\BudgetAssignData;
use App\Domains\Budget\Data\BudgetReservedNames;
use Insane\Journal\Models\Core\AccountDetailType;
diff --git a/app/Domains/Journal/Actions/AccountDelete.php b/app/Domains/Journal/Actions/AccountDelete.php
index c6fdda41..825ccc25 100644
--- a/app/Domains/Journal/Actions/AccountDelete.php
+++ b/app/Domains/Journal/Actions/AccountDelete.php
@@ -4,9 +4,9 @@
use Illuminate\Foundation\Auth\User;
use Illuminate\Support\Facades\Gate;
-use Illuminate\Validation\ValidationException;
-use Insane\Journal\Contracts\AccountDeletes;
use Insane\Journal\Models\Core\Account;
+use Insane\Journal\Contracts\AccountDeletes;
+use Illuminate\Validation\ValidationException;
class AccountDelete implements AccountDeletes
{
@@ -18,7 +18,7 @@ public function delete(User $user, Account $account)
public function validate(User $user, mixed $account)
{
- Gate::forUser($user)->authorize('delete-account', $account);
+ Gate::forUser($user)->authorize('delete', $account);
if (count($account->transactions)) {
throw ValidationException::withMessages([
'account' => __('You may not delete account with transactions.'),
diff --git a/app/Domains/Journal/Policies/AccountPolicy.php b/app/Domains/Journal/Policies/AccountPolicy.php
index eb689157..6b044d62 100644
--- a/app/Domains/Journal/Policies/AccountPolicy.php
+++ b/app/Domains/Journal/Policies/AccountPolicy.php
@@ -3,8 +3,8 @@
namespace App\Domains\Journal\Policies;
use App\Models\User;
-use Illuminate\Auth\Access\HandlesAuthorization;
use Insane\Journal\Models\Core\Account;
+use Illuminate\Auth\Access\HandlesAuthorization;
class AccountPolicy
{
@@ -32,6 +32,7 @@ public function update(User $user, Account $account)
public function delete(User $user, Account $account)
{
+ dd($account);
return $user->team_id == $account->team_id;
}
}
diff --git a/app/Domains/Transaction/Models/BillingCycle.php b/app/Domains/Transaction/Models/BillingCycle.php
new file mode 100644
index 00000000..953ba40b
--- /dev/null
+++ b/app/Domains/Transaction/Models/BillingCycle.php
@@ -0,0 +1,103 @@
+hasMany(TransactionLine::class, 'account_id')
+ ->whereBetween("date", [$this->from, $this->until]);
+ }
+
+ // Transactionable config
+ public function getTransactionItems() {
+ return [];
+ }
+
+ public static function getCategoryName($payable): string {
+ return "credit_cards";
+ }
+
+ public function getTransactionDescription() {
+ return "Balance de credito";
+ }
+
+ public function getTransactionDirection(): string {
+ return Transaction::DIRECTION_CREDIT;
+ }
+
+ public function getAccountId() {
+ return $this->account_id;
+ }
+
+ public function getCounterAccountId(): int {
+ return $this->client_account_id;
+ }
+
+ // payable config
+ public function getStatusField(): string {
+ return 'payment_status';
+ }
+
+ public static function calculateTotal($payable) {
+ $payable->total = $payable->transactions()->sum('amount');
+ $payable->due = $payable->total - $payable->paid;
+ }
+
+ public static function checkStatus($payable) {
+ $debt = $payable->total - $payable->paid;
+ if ($debt <= 0) {
+ $status = self::STATUS_PAID;
+ } elseif ($debt > 0 && $debt < $payable->amount) {
+ $status = self::STATUS_PARTIALLY_PAID;
+ } elseif ($debt && $payable->hasLateInstallments()) {
+ $status = self::STATUS_LATE;
+ } elseif ($debt && !$payable->cancelled_at) {
+ $status = self::STATUS_PENDING;
+ } elseif ($payable->cancelled_at) {
+ $status = self::STATUS_CANCELLED;
+ } else {
+ $status = $payable->status;
+ }
+ $payable->status = $status;
+ }
+
+ public function updateStatus() {
+ self::checkPayments($this);
+ $this->save();
+ self::calculateTotal($this);
+ self::checkStatus($this);
+ }
+
+ public function getConceptLine(): string {
+ return "";
+ }
+}
diff --git a/app/Domains/Transaction/Services/CreditCardReportService.php b/app/Domains/Transaction/Services/CreditCardReportService.php
index cdde5be0..70c3174f 100644
--- a/app/Domains/Transaction/Services/CreditCardReportService.php
+++ b/app/Domains/Transaction/Services/CreditCardReportService.php
@@ -2,21 +2,37 @@
namespace App\Domains\Transaction\Services;
+use Exception;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
+use App\Domains\AppCore\Models\Category;
+use App\Domains\Budget\Data\BudgetReservedNames;
+use App\Domains\Transaction\Models\BillingCycle;
class CreditCardReportService
{
- public function creditCardCycleByAccount($teamId, $date, $creditCardId = null) {
- $endCycleDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonth(1)->format('Y-m-d');
- $startCycleDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonth(2)->format('Y-m-d');
+ public function creditCardCycleByAccount($teamId, $date, $monthsBack = 1, $creditCardId = null, $asToday = null) {
+ $endCycleDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonth($monthsBack)->format('Y-m-d');
+ $startCycleDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonth($monthsBack+1)->format('Y-m-d');
+
+ $readyToAssign = Category::where([
+ 'team_id' => $teamId,
+ ])
+ ->where('name', BudgetReservedNames::READY_TO_ASSIGN->value)
+ ->first();
return DB::table(DB::raw('accounts a'))
->where("a.team_id", $teamId)
->whereNotNull('a.credit_closing_day')
+ ->when($asToday, fn ($q) => $q->whereRaw("now() >= DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0')))", [
+ $endCycleDate
+ ]))
->selectRaw("
+ ABS(COALESCE(SUM(CASE WHEN tl.type = -1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS subtotal,
ABS(COALESCE(SUM(tl.amount * tl.type), 0)) AS total,
+ ABS(COALESCE(SUM(CASE WHEN tl.type = 1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS discount,
a.name,
+ a.id,
a.credit_limit,
DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) AS `from`,
DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) AS `until`", [
@@ -24,28 +40,171 @@ public function creditCardCycleByAccount($teamId, $date, $creditCardId = null) {
$endCycleDate
])
- ->leftJoin(DB::raw('transaction_lines tl'), fn ($q) => $q->on('a.id', 'tl.account_id')->where('tl.type', -1))
+ ->leftJoin(DB::raw('transaction_lines tl'), fn ($q) => $q->on('a.id', 'tl.account_id')
+ ->whereRaw('(tl.type = ? or tl.category_id = ?)', [-1, $readyToAssign->id])
+ )
// ->where('account_id', $accountId)
- ->whereRaw("tl.date >= DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0')))
- AND tl.date < DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0')))", [
+ ->whereRaw("(tl.date >= DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) and tl.type = -1
+ AND
+ tl.date < DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0')))
+ )
+ OR tl.date = DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) AND tl.type = 1", [
$startCycleDate,
+ $endCycleDate,
$endCycleDate
])
->groupBy('a.id')
->get();
}
+ public function getTopCategoriesByAccount($teamId, $startDate = 1, $endDate, $creditCardId = null) {
+ return DB::table(DB::raw('accounts a'))
+ ->where("a.team_id", $teamId)
+ ->whereNotNull('a.credit_closing_day')
+ ->selectRaw("
+ ABS(COALESCE(SUM(CASE WHEN tl.type = -1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS subtotal,
+ ABS(COALESCE(SUM(tl.amount * tl.type), 0)) AS total,
+ ABS(COALESCE(SUM(CASE WHEN tl.type = 1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS discount,
+ a.name,
+ a.id,
+ c.name as cat_name
+ ")
+ ->leftJoin(DB::raw('transaction_lines tl'), fn ($q) => $q->on('a.id', 'tl.account_id')
+ ->whereNotNull('tl.category_id')
+ )
+ ->join(DB::raw('categories c'),'tl.category_id', 'c.id')
+ ->whereBetween("tl.date", [$startDate, $endDate])
+ ->groupBy(DB::raw('a.id, c.name'))
+ ->orderBy('total', 'desc')
+ ->get();
+ }
+
+ public function getTopCategoriesByCreditCard($teamId, $startDate = 1, $endDate, $creditCardId = null) {
+ $readyToAssign = Category::where([
+ 'team_id' => $teamId,
+ ])
+ ->where('name', BudgetReservedNames::READY_TO_ASSIGN->value)
+ ->first();
+
+ return DB::select("
+ WITH TopCategories as (
+ SELECT
+ ABS(COALESCE(SUM(CASE WHEN tl.type = -1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS subtotal,
+ ABS(COALESCE(SUM(tl.amount * tl.type), 0)) AS total,
+ ABS(COALESCE(SUM(CASE WHEN tl.type = 1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS discount,
+ a.name,
+ a.id,
+ c.name as cat_name,
+ ROW_NUMBER() OVER (PARTITION BY tl.account_id ORDER BY SUM(tl.amount * tl.type)) AS rank
+ FROM transaction_lines tl
+ INNER JOIN transactions t on tl.transaction_id = t.id AND t.status = 'verified' AND tl.category_id <> :readyToAssign
+ INNER JOIN accounts a on tl.account_id = a.id AND a.credit_closing_day IS NOT NULL
+ INNER JOIN categories c on c.id = tl.category_id
+ WHERE tl.date >= :startDate AND tl.date <= :endDate
+ AND tl.team_id = :teamId
+ GROUP BY tl.account_id, tl.category_id
+ ORDER BY a.id, ABS(COALESCE(SUM(tl.amount * tl.type), 0))
+ )
+ SELECT
+ tc.name,
+ tc.cat_name,
+ tc.total
+ FROM TopCategories tc
+ WHERE tc.rank <= 4
+ GROUP BY tc.id, tc.cat_name
+ ORDER BY tc.id, tc.rank", [
+ 'teamId' => $teamId,
+ 'startDate' => $startDate,
+ 'endDate' => $endDate,
+ 'readyToAssign' => $readyToAssign->id
+ ]);
+ }
+
+ public function getTopPayeesByAccount($teamId, $date, $monthsBack = 1, $creditCardId = null, $asToday = null) {
+ $endCycleDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonth($monthsBack)->format('Y-m-d');
+ $startCycleDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonth($monthsBack+1)->format('Y-m-d');
+
+ $readyToAssign = Category::where([
+ 'team_id' => $teamId,
+ ])
+ ->where('name', BudgetReservedNames::READY_TO_ASSIGN->value)
+ ->first();
+
+ return DB::table(DB::raw('accounts a'))
+ ->where("a.team_id", $teamId)
+ ->whereNotNull('a.credit_closing_day')
+ ->when($asToday, fn ($q) => $q->whereRaw("now() >= DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0')))", [
+ $endCycleDate
+ ]))
+ ->selectRaw("
+ ABS(COALESCE(SUM(CASE WHEN tl.type = -1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS subtotal,
+ ABS(COALESCE(SUM(tl.amount * tl.type), 0)) AS total,
+ ABS(COALESCE(SUM(CASE WHEN tl.type = 1 THEN tl.amount * tl.type ELSE 0 END), 0)) AS discount,
+ a.name,
+ a.id,
+ a.credit_limit,
+ DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) AS `from`,
+ DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) AS `until`", [
+ $startCycleDate,
+ $endCycleDate
+ ])
+
+ ->leftJoin(DB::raw('transaction_lines tl'), fn ($q) => $q->on('a.id', 'tl.account_id')
+ ->whereRaw('(tl.type = ? or tl.category_id = ?)', [-1, $readyToAssign->id])
+ )
+ // ->where('account_id', $accountId)
+ ->whereRaw("(tl.date >= DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) and tl.type = -1
+ AND
+ tl.date < DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0')))
+ )
+ OR tl.date = DATE_FORMAT(?, CONCAT('%Y-%m-', LPAD(a.credit_closing_day, 2, '0'))) AND tl.type = 1", [
+ $startCycleDate,
+ $endCycleDate,
+ $endCycleDate
+ ])
+ ->groupBy('a.id')
+ ->get();
+ }
+
+ public function getBillingCyclesByCardInPeriod($teamId, $startDate = 1, $endDate, $creditCardId = null) {
+ $billingData = DB::table(DB::raw('accounts a'))
+ ->where("a.team_id", $teamId)
+ ->whereNotNull('a.credit_closing_day')
+ ->selectRaw("
+ ABS(COALESCE(SUM(bc.subtotal), 0)) AS subtotal,
+ ABS(COALESCE(SUM(bc.total), 0)) AS total,
+ ABS(COALESCE(SUM(bc.discounts), 0)) AS discounts,
+ a.name,
+ a.id
+ ")
+ ->leftJoin(DB::raw('billing_cycles bc'), 'a.id', 'bc.account_id')
+ ->whereBetween("bc.end_at", [$startDate, $endDate])
+ ->groupBy('a.id')
+ ->get();
+
+ return [
+ "data" => $billingData,
+ "discountTotal" => $billingData->sum('discount'),
+ "total" => $billingData->sum('total'),
+ "subtotal" => $billingData->sum('subtotal')
+ ];
+ }
+
public function creditCards($teamId, $date)
{
$lastCycleBalances = $this->creditCardCycleByAccount($teamId, $date);
$creditTotal = $lastCycleBalances->sum('total');
+ $startPeriodDate = Carbon::createFromFormat('Y-m-d', $date)->startOfMonth()->subMonths(12);
+
+ // dd($this->getTopCategoriesByCreditCard($teamId, $startPeriodDate, $date));
+
$data = [
'lastCycleBalances' => $lastCycleBalances,
"creditTotal" => $creditTotal,
"creditLineUsage" => round($creditTotal / $lastCycleBalances->sum('credit_limit') * 100, 2),
- 'topCategoriesByCard' => [],
- 'rewardsByCardInPeriod' => [],
+ 'topCategoriesByCard' => $this->getTopCategoriesByCreditCard($teamId, $startPeriodDate, $date),
+ 'billingCyclesByCard' => $this->getBillingCyclesByCardInPeriod($teamId, $startPeriodDate, $date),
'topPayeesByCard' => [],
'topTransaction' => [],
'billingCyclePayments' => [],
@@ -54,5 +213,42 @@ public function creditCards($teamId, $date)
return $data;
}
+ public function generateBillingCycles($teamId, $yearMonth) {
+ $monthsWithTransactions = DB::table('transaction_lines')
+ ->selectRaw("date_format(transaction_lines.date, '%Y-%m') AS date")
+ ->groupBy(DB::raw("date_format(transaction_lines.date, '%Y-%m')"))
+ ->whereRaw("date_format(transaction_lines.date, '%Y-%m') >= ?", [$yearMonth])
+ ->get()
+ ->pluck('date');
+
+ $monthsWithTransactions = [
+ ...$monthsWithTransactions,
+ ];
+
+ $total = count($monthsWithTransactions);
+ $count = 0;
+
+ foreach ($monthsWithTransactions as $month) {
+ $lastCycleBalances = $this->creditCardCycleByAccount($teamId, $month."-01", 0, null, true)->sortBy('from');
+ $count++;
+ foreach ($lastCycleBalances as $creditCardAccount) {
+ BillingCycle::updateOrCreate([
+ "account_id" => $creditCardAccount->id,
+ "team_id" => $teamId,
+ "user_id" => 0,
+ "start_at" => $creditCardAccount->from,
+ "end_at" => $creditCardAccount->until,
+ ], [
+ "minimum_payment" => $creditCardAccount->total,
+ "subtotal" => $creditCardAccount->subtotal,
+ "discounts" =>$creditCardAccount->discount,
+ "total" => $creditCardAccount->total,
+ "due_at" => $creditCardAccount->until
+ ]);
+ }
+ echo "updated month {$month}".PHP_EOL;
+ echo "{$count} of {$total}".PHP_EOL.PHP_EOL;
+ }
+ }
}
diff --git a/database/migrations/2024_06_23_183947_credit_card.php b/database/migrations/2024_06_23_183947_credit_card.php
new file mode 100644
index 00000000..6bdad430
--- /dev/null
+++ b/database/migrations/2024_06_23_183947_credit_card.php
@@ -0,0 +1,39 @@
+id();
+ $table->foreignId('team_id');
+ $table->foreignId('user_id');
+ $table->foreignId('account_id');
+ $table->date('start_at');
+ $table->date('end_at');
+ $table->date('due_at');
+ $table->decimal('minimum_payment', 11, 4)->default(0);;
+ $table->decimal('due', 11, 4)->default(0);;
+ $table->decimal('subtotal', 11, 4)->default(0);;
+ $table->decimal('discount', 11, 4)->default(0);;
+ $table->decimal('total', 11, 4)->default(0);;
+ $table->string('status')->default('pending');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ //
+ }
+};
diff --git a/resources/js/Components/ChartTopCreditCard.vue b/resources/js/Components/ChartTopCreditCard.vue
new file mode 100644
index 00000000..0baebe0a
--- /dev/null
+++ b/resources/js/Components/ChartTopCreditCard.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+ {{ header.label }}
+
+
+
+ {{ formatMoney(value) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/Components/molecules/WidgetTitleCard.vue b/resources/js/Components/molecules/WidgetTitleCard.vue
index d27f5ad2..1f07634a 100644
--- a/resources/js/Components/molecules/WidgetTitleCard.vue
+++ b/resources/js/Components/molecules/WidgetTitleCard.vue
@@ -4,13 +4,13 @@ import { AtButton } from "atmosphere-ui";
withDefaults(defineProps<{
title: string;
- action: {
+ action?: {
label: string,
iconClass?: string,
},
- hideDivider: boolean;
- withPadding: boolean;
- border: boolean
+ hideDivider?: boolean;
+ withPadding?: boolean;
+ border?: boolean
}>(), {
withPadding: true,
border: true,
diff --git a/resources/js/Components/organisms/DonutChart.vue b/resources/js/Components/organisms/DonutChart.vue
index f1714b25..0c16668d 100644
--- a/resources/js/Components/organisms/DonutChart.vue
+++ b/resources/js/Components/organisms/DonutChart.vue
@@ -1,6 +1,6 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/utils/index.ts b/resources/js/utils/index.ts
index 93632ad4..1640391b 100644
--- a/resources/js/utils/index.ts
+++ b/resources/js/utils/index.ts
@@ -163,3 +163,17 @@ export const getRangeData = (range: RangeValue[]|null, direction = 'back') => {
}
return rangeData;
}
+
+export const nameToColor = (name: string) => {
+ let hash = 0;
+ for (let i = 0; i < name?.length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ const color = Math.abs(hash).toString(16).substring(0, 6);
+ return "#" + "0".repeat(6 - color.length) + color; // pad with zeros if necessary
+ }
+
+ export const getOrGenerateColor = (index: number, name: string) => {
+ const colors = ["#00C4B3", "#5B5293", "#FFB4AA"]
+ return colors[index] ?? nameToColor(name);
+ }
diff --git a/tests/Feature/CreditCard/CreditCardCronsTest.php b/tests/Feature/CreditCard/CreditCardCronsTest.php
new file mode 100644
index 00000000..5d2d359d
--- /dev/null
+++ b/tests/Feature/CreditCard/CreditCardCronsTest.php
@@ -0,0 +1,23 @@
+createCreditCard([
+ "name" => "Visa Gold",
+ "category_id" => null,
+ "account_detail_type_id" => null,
+ "description" => 0,
+ "opening_balance" => 0,
+ "credit_closing_day" => 15,
+ "credit_limit" => 10000,
+ ]);
+
+ $this->artisan('background:generate-billing-cycles')->assertSuccessful();
+ $this->assertGreaterThan(0, BillingCycle::all());
+ }
+}
diff --git a/tests/Feature/CreditCard/CreditCardTest.php b/tests/Feature/CreditCard/CreditCardTest.php
new file mode 100644
index 00000000..188dc7d9
--- /dev/null
+++ b/tests/Feature/CreditCard/CreditCardTest.php
@@ -0,0 +1,73 @@
+actingAs($this->user);
+
+ $response = $this->get('/loans');
+
+ $response->assertStatus(200);
+ }
+
+ public function testItShouldNotCreateLoanWithoutSourceAccountId() {
+ $this->actingAs($this->user);
+
+ $response = $this->post('/loans', array_merge(
+ $this->loanData, [
+ 'source_account_id' => null
+ ]));
+
+ $response->assertSessionHasErrors(['source_account_id']);
+
+ }
+
+ public function testItShouldNotCreateLoanWithoutFunds() {
+ $this->actingAs($this->user);
+
+ $response = $this->withoutExceptionHandling()->post('/loans', $this->loanData);
+ $response->assertSessionHasErrors();
+ }
+
+ public function testItShouldCreateLoan() {
+ $this->actingAs($this->user);
+ $this->fundAccount('daily_box', $this->loanData['amount'], $this->lender->team_id);
+ $response = $this->post('/loans', $this->loanData);
+ $response->assertStatus(302);
+ $this->assertCount(1, Loan::all());
+ }
+
+ public function testLoanShouldHaveStatusPending() {
+ $this->actingAs($this->user);
+ $this->fundAccount('daily_box', $this->loanData['amount'], $this->lender->team_id);
+
+ $response = $this->post('/loans', $this->loanData);
+ $response->assertStatus(302);
+ $loan = Loan::query()->first();
+
+ $this->assertEquals($loan->payment_status, Loan::STATUS_PENDING);
+ }
+
+ public function testClientShouldHaveStatusActive() {
+ $this->actingAs($this->user);
+ $this->fundAccount('daily_box', $this->loanData['amount'], $this->lender->team_id);
+
+ $response = $this->post('/loans', $this->loanData);
+ $response->assertStatus(302);
+ $loan = Loan::query()->first();
+
+ $this->assertEquals($loan->client->status, ClientStatus::Active);
+ }
+}
diff --git a/tests/Feature/CreditCard/Helpers/CreditCardBase.php b/tests/Feature/CreditCard/Helpers/CreditCardBase.php
new file mode 100644
index 00000000..bf715ad3
--- /dev/null
+++ b/tests/Feature/CreditCard/Helpers/CreditCardBase.php
@@ -0,0 +1,63 @@
+seed();
+ $user = User::factory()->withPersonalTeam()->create();
+ $user->current_team_id = $user->fresh()->ownedTeams()->latest('id')->first()->id;
+ $user->save();
+ $this->user = $user;
+
+ $this->creditCardData = self::getData($this->user, []);
+ }
+
+ public function fundAccount(string $accountDisplayId, int $amount, $teamId) {
+ Account::findByDisplayId($accountDisplayId, $teamId)->openBalance($amount);
+ }
+
+ public function createCreditCard(mixed $formData = []) {
+ $this->actingAs($this->user);
+
+ $this->post('/accounts?json=true', self::getData($this->user, [
+ ...$formData,
+ ""
+ ]));
+
+ return Account::latest()->first();
+ }
+
+ public static function getData(User $user, $formData = []) {
+ return [
+ ...$formData,
+ 'user_id' => $user->id,
+ 'team_id' => $user->current_team_id,
+ 'display_id' => $formData['display_id'] ?? null,
+ "account_detail_type_id" => AccountDetailType::where([
+ 'name' => AccountDetailType::CREDIT_CARD
+ ])->select('id')->get()->id,
+ 'name' => $formData['name'] ?? null,
+ 'description' => $formData['description'] ?? "",
+ 'currency_code' => $formData['currency_code'] ?? "DOP",
+ ];
+ }
+}