@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddRefundSourcesAndPayouts extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->ensureRefundSourceColumns();
|
||||
$this->ensureRefundPayoutsTable();
|
||||
$this->ensureRefundPayoutFingerprintColumns();
|
||||
$this->backfillRefundSources();
|
||||
$this->backfillLegacyPayouts();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->db->tableExists('refund_payouts')) {
|
||||
$this->forge->dropTable('refund_payouts', true);
|
||||
}
|
||||
|
||||
if ($this->db->tableExists('refunds')) {
|
||||
foreach (['source_type', 'source_id', 'requested_amount_cents', 'approved_amount_cents', 'currency'] as $column) {
|
||||
if ($this->db->fieldExists($column, 'refunds')) {
|
||||
$this->forge->dropColumn('refunds', $column);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureRefundSourceColumns(): void
|
||||
{
|
||||
if (!$this->db->tableExists('refunds')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
if (!$this->db->fieldExists('source_type', 'refunds')) {
|
||||
$columns['source_type'] = ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true, 'after' => 'request'];
|
||||
}
|
||||
if (!$this->db->fieldExists('source_id', 'refunds')) {
|
||||
$columns['source_id'] = ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true, 'after' => 'source_type'];
|
||||
}
|
||||
if (!$this->db->fieldExists('requested_amount_cents', 'refunds')) {
|
||||
$columns['requested_amount_cents'] = ['type' => 'INT', 'constraint' => 11, 'null' => true, 'after' => 'refund_amount'];
|
||||
}
|
||||
if (!$this->db->fieldExists('approved_amount_cents', 'refunds')) {
|
||||
$columns['approved_amount_cents'] = ['type' => 'INT', 'constraint' => 11, 'null' => true, 'after' => 'requested_amount_cents'];
|
||||
}
|
||||
if (!$this->db->fieldExists('currency', 'refunds')) {
|
||||
$columns['currency'] = ['type' => 'CHAR', 'constraint' => 3, 'null' => false, 'default' => 'USD', 'after' => 'approved_amount_cents'];
|
||||
}
|
||||
|
||||
if ($columns !== []) {
|
||||
$this->forge->addColumn('refunds', $columns);
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureRefundPayoutsTable(): void
|
||||
{
|
||||
if ($this->db->tableExists('refund_payouts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
|
||||
'refund_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false],
|
||||
'amount_cents' => ['type' => 'INT', 'constraint' => 11, 'null' => false],
|
||||
'currency' => ['type' => 'CHAR', 'constraint' => 3, 'null' => false, 'default' => 'USD'],
|
||||
'payout_type' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => false, 'default' => 'cash_out'],
|
||||
'payment_method' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true],
|
||||
'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => false],
|
||||
'external_reference' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
|
||||
'check_number' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
|
||||
'check_date' => ['type' => 'DATE', 'null' => true],
|
||||
'evidence_path' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'idempotency_key' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => false],
|
||||
'operation_type' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||
'request_fingerprint_hash' => ['type' => 'CHAR', 'constraint' => 64, 'null' => true],
|
||||
'processed_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
|
||||
'processed_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'reversed_payout_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
|
||||
'failure_code' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
|
||||
'failure_message' => ['type' => 'TEXT', 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => false],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => false],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addKey('refund_id');
|
||||
$this->forge->addKey('reversed_payout_id');
|
||||
$this->forge->addUniqueKey('idempotency_key', 'uniq_refund_payouts_idempotency_key');
|
||||
$this->forge->createTable('refund_payouts', true);
|
||||
}
|
||||
|
||||
private function ensureRefundPayoutFingerprintColumns(): void
|
||||
{
|
||||
if (!$this->db->tableExists('refund_payouts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
if (!$this->db->fieldExists('operation_type', 'refund_payouts')) {
|
||||
$columns['operation_type'] = [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
'after' => 'idempotency_key',
|
||||
];
|
||||
}
|
||||
if (!$this->db->fieldExists('request_fingerprint_hash', 'refund_payouts')) {
|
||||
$columns['request_fingerprint_hash'] = [
|
||||
'type' => 'CHAR',
|
||||
'constraint' => 64,
|
||||
'null' => true,
|
||||
'after' => 'operation_type',
|
||||
];
|
||||
}
|
||||
|
||||
if ($columns !== []) {
|
||||
$this->forge->addColumn('refund_payouts', $columns);
|
||||
}
|
||||
}
|
||||
|
||||
private function backfillRefundSources(): void
|
||||
{
|
||||
if (!$this->db->tableExists('refunds')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->query(
|
||||
"UPDATE `refunds`
|
||||
SET `requested_amount_cents` = ROUND(COALESCE(`refund_amount`, 0) * 100),
|
||||
`approved_amount_cents` = ROUND(COALESCE(`refund_amount`, 0) * 100),
|
||||
`currency` = COALESCE(NULLIF(`currency`, ''), 'USD')
|
||||
WHERE `requested_amount_cents` IS NULL OR `approved_amount_cents` IS NULL"
|
||||
);
|
||||
|
||||
if ($this->db->fieldExists('source_type', 'refunds')) {
|
||||
$this->db->query(
|
||||
"UPDATE `refunds`
|
||||
SET `source_type` = CASE
|
||||
WHEN `invoice_id` IS NOT NULL THEN 'invoice_overpayment'
|
||||
WHEN LOWER(COALESCE(`request`, '')) = 'duplicate' THEN 'payment_duplicate'
|
||||
ELSE 'administrative_credit'
|
||||
END,
|
||||
`source_id` = CASE
|
||||
WHEN `invoice_id` IS NOT NULL THEN `invoice_id`
|
||||
ELSE `source_id`
|
||||
END
|
||||
WHERE `source_type` IS NULL"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function backfillLegacyPayouts(): void
|
||||
{
|
||||
if (!$this->db->tableExists('refunds') || !$this->db->tableExists('refund_payouts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refunds = $this->db->table('refunds')
|
||||
->select('id, refund_paid_amount, currency, refund_method, check_nbr, check_file, refunded_at, updated_by, created_at, updated_at')
|
||||
->where('refund_paid_amount >', 0)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($refunds as $refund) {
|
||||
$refundId = (int) ($refund['id'] ?? 0);
|
||||
if ($refundId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$exists = $this->db->table('refund_payouts')
|
||||
->where('refund_id', $refundId)
|
||||
->where('idempotency_key', 'legacy-refund-' . $refundId)
|
||||
->countAllResults();
|
||||
if ($exists > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->table('refund_payouts')->insert([
|
||||
'refund_id' => $refundId,
|
||||
'amount_cents' => (int) round(((float) ($refund['refund_paid_amount'] ?? 0)) * 100),
|
||||
'currency' => (string) ($refund['currency'] ?? 'USD') ?: 'USD',
|
||||
'payout_type' => 'cash_out',
|
||||
'payment_method' => $refund['refund_method'] ?? null,
|
||||
'status' => 'completed',
|
||||
'external_reference' => 'legacy_import',
|
||||
'check_number' => $refund['check_nbr'] ?? null,
|
||||
'check_date' => null,
|
||||
'evidence_path' => $refund['check_file'] ?? null,
|
||||
'idempotency_key' => 'legacy-refund-' . $refundId,
|
||||
'operation_type' => 'refund_payout',
|
||||
'request_fingerprint_hash' => hash('sha256', json_encode([
|
||||
'operation_type' => 'refund_payout',
|
||||
'refund_id' => $refundId,
|
||||
'amount_cents' => (int) round(((float) ($refund['refund_paid_amount'] ?? 0)) * 100),
|
||||
'payment_method' => $refund['refund_method'] ?? null,
|
||||
'currency' => (string) ($refund['currency'] ?? 'USD') ?: 'USD',
|
||||
'external_reference' => 'legacy_import',
|
||||
], JSON_UNESCAPED_SLASHES)),
|
||||
'processed_by' => $refund['updated_by'] ?? null,
|
||||
'processed_at' => $refund['refunded_at'] ?? $refund['updated_at'] ?? $now,
|
||||
'reversed_payout_id' => null,
|
||||
'failure_code' => null,
|
||||
'failure_message' => null,
|
||||
'created_at' => $refund['created_at'] ?? $now,
|
||||
'updated_at' => $refund['updated_at'] ?? $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user