Initial commit - WASL Digital Wallet

This commit is contained in:
Hamza-Ayed
2026-06-20 21:55:06 +03:00
commit 7306c47368
61 changed files with 4157 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Models;
use App\Enums\TransactionStatus;
use App\Enums\TransactionType;
use App\Models\Traits\HasMinorUnits;
use App\Models\Traits\HasUuid;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
* transactions — IMMUTABLE LEDGER
*
* Once status = 'completed', this row is NEVER updated.
* Reversals are new rows with type = 'refund'.
*
* @property string $uuid
* @property string $reference_code WASL-XXXXXXXX (user-visible)
* @property string $type TransactionType enum
* @property string $status TransactionStatus enum
* @property int|null $debit_wallet_id
* @property int|null $credit_wallet_id
* @property int $amount_minor BIGINT — never float/decimal
* @property int $fee_minor BIGINT
* @property string $currency_code
* @property string|null $description
* @property array|null $metadata
* @property string $idempotency_key
* @property string|null $initiator_ip
* @property string|null $device_id
*/
class Transaction extends BaseModel
{
use HasFactory;
use HasUuid;
use HasMinorUnits;
protected $fillable = [
'uuid',
'reference_code',
'type',
'status',
'debit_wallet_id',
'credit_wallet_id',
'amount_minor',
'fee_minor',
'currency_code',
'description',
'metadata',
'idempotency_key',
'initiator_ip',
'device_id',
'initiated_at',
'completed_at',
'failure_reason',
];
protected $casts = [
'type' => TransactionType::class,
'status' => TransactionStatus::class,
'amount_minor' => 'integer',
'fee_minor' => 'integer',
'metadata' => 'array',
'initiated_at' => 'datetime',
'completed_at' => 'datetime',
];
// ── Relationships ──
public function debitWallet()
{
return $this->belongsTo(Wallet::class, 'debit_wallet_id');
}
public function creditWallet()
{
return $this->belongsTo(Wallet::class, 'credit_wallet_id');
}
public function entries()
{
return $this->hasMany(TransactionEntry::class);
}
public function fraudAlerts()
{
return $this->hasMany(FraudAlert::class);
}
// ── Scopes ──
public function scopePending($query)
{
return $query->where('status', TransactionStatus::PENDING);
}
public function scopeCompleted($query)
{
return $query->where('status', TransactionStatus::COMPLETED);
}
public function scopeFailed($query)
{
return $query->where('status', TransactionStatus::FAILED);
}
public function scopeForWallet($query, int $walletId)
{
return $query->where('debit_wallet_id', $walletId)
->orWhere('credit_wallet_id', $walletId);
}
public function scopeInFlight($query)
{
return $query->whereIn('status', [
TransactionStatus::PENDING,
TransactionStatus::PROCESSING,
]);
}
// ── Helpers ──
public function formattedAmount(): string
{
return self::formatMoney($this->amount_minor, $this->currency_code);
}
public function formattedFee(): string
{
return self::formatMoney($this->fee_minor, $this->currency_code);
}
public function isFinal(): bool
{
return $this->status->isFinal();
}
/**
* Generate a unique reference code: WASL-XXXXXXXX
* Uses random alphanumeric characters for human readability.
*/
public static function generateReferenceCode(): string
{
$prefix = config('wasl.reference.prefix', 'WASL');
$length = config('wasl.reference.length', 8);
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I to avoid confusion
do {
$code = $prefix . '-' . substr(str_shuffle($chars), 0, $length);
} while (self::where('reference_code', $code)->exists());
return $code;
}
}