feat: باقي المجموعة I — أمان السحب (OTP + بصمة + HMAC + تدقيق)
I4 — OTP على السحب عبر نبيه. تدفّق خطوتين، وقاعدته: **لا يتحرك مال قبل إثبات الهوية**: - POST /payouts/request → يرسل رمزاً، حالة pending_otp، **بلا خصم** - POST /payouts/:id/confirm → يتحقق ثم يحجز ذرّياً (خصم I1) - حدّ 5 محاولات (رمز 4 خانات يُخمَّن في دقائق بلا حدّ)، الرمز يُستهلك مرة - complete يرفض طلباً بلا otp_verified_at (حارس ضد تخطّي التأكيد) - fail لا يردّ مالاً لطلب pending_otp — لم يُخصم منه شيء، وردّه يخلق مالاً I5 — biometric_method/at + device_id + request_ip على السحب. أثرٌ للتحقيق لا مصادقة: العميل يستطيع ادّعاءها، والمصادقة الحقيقية JWT + رمز واتساب. I6 — HMAC **مبنيّ ومطفأ** (PAYMENTS_REQUIRE_SIGNATURE=false) حتى يوقّع فلاتر؛ تفعيله الآن يقطع كل سحب. مفتاح **لكل جلسة** يُصدره الدخول لا سرّ ثابت في التطبيق (الثابت يُستخرج بالهندسة العكسية فيصير التوقيع مسرحية). يوقّع timestamp.METHOD.path.body بنافذة 5 دقائق؛ rawBody مفعّل في main. I7 — tripz_audit_log append-only: من·ماذا·متى·أي IP وجهاز. لا يرمي أبداً — فشل التدقيق يجب ألّا يُسقط عمليةً مالية نجحت. I2 أُلغيت الحاجة إليها: إيراد المنصة = الشحن، و credit_txns دفتره فعلاً؛ محفظة ثانية = دفتر مزدوج يحتاج مطابقة. الباقي تقرير لا محفظة. I3/I8 مؤجَّلتان بوعي (توثيق في docs/17). wallet-race-test.mjs حُدِّث: السباق انتقل من request إلى confirm، وأُضيف محكّ أن الطلب وحده لا يمسّ الرصيد. هجرة: PayoutSecurityAndAudit (تعتبر السحوبات القائمة مُتحقَّقة وإلا رفض complete صرفها للأبد). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
91e1692457
commit
1605754722
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* سجل التدقيق المالي (docs/17 — I7؛ مكافئ `admin_audit_log` عند سيرو).
|
||||
* الجدول: tripz_audit_log. **append-only** — لا تحديث ولا حذف.
|
||||
*
|
||||
* يجيب عن سؤال واحد عند كل نزاع: **من فعل ماذا ومتى ومن أين؟**
|
||||
*/
|
||||
@Entity('audit_log')
|
||||
@Index(['tenant_id', 'created_at'])
|
||||
@Index(['tenant_id', 'subject_type', 'subject_id'])
|
||||
export class AuditLog {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
tenant_id: string;
|
||||
|
||||
/** من نفّذ الفعل — قد يكون السائق نفسه أو أدمن أو `null` للنظام. */
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
actor_user_id: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
actor_role: string | null;
|
||||
|
||||
/** payout.request · payout.confirm · payout.complete · payout.fail · credit.topup … */
|
||||
@Column()
|
||||
action: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
subject_type: string | null; // payout | credit | wallet
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
subject_id: string | null;
|
||||
|
||||
@Column({ type: 'numeric', precision: 12, scale: 3, nullable: true })
|
||||
amount: number | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
currency: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', default: {} })
|
||||
meta: Record<string, any>;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
ip: string | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user