الأبحاث · جولةُ البرمجة الأولى

CB-3-07 — API وتكامل · Go · عالٍ

جولةُ البرمجة الأولى · 2026-10-29 · GAIBRA v2.32.03 · واجبُ التشغيل

نتيجةُ الجولة

الترتيبالمتنافسالرمز أمام المحكّمالكلماتالاختباراتOpusGrokالمجموعالصدق (Opus · Grok)
1GAIBRAب3,26714/1495.096.095.511 · 12
2Claude Sonnetهـ1,83514/1490.091.090.511 · 11
3ChatGPTج1,69414/1481.088.084.59 · 10
4DeepSeekأ1,68614/1474.088.081.07 · 10
5Geminiد1,06914/1465.070.067.54 · 7

نصُّ السيناريو كما كتبه المولّد

نستقبل إشعارات نجاح/فشل الدفع من بوابتين: «مدى-بي» (JSON جسم الصفحة، توقيع HMAC-SHA256 في ترويسة X-Mada-Signature بصيغة sha256=<hex>) و«باي-لاين» (التوقيع في الحقل sign داخل الجسم نفسه، وهو HMAC-SHA256 لقيم الحقول المرتبة أبجدياً بفاصل \&، بمفتاح مختلف). المشكلة: البوابتان تستخدمان أسماء حقول مختلفة، وحالات مختلفة، وتعيدان إرسال نفس الإشعار (retry) حتى نرد 200، فوصلتنا تحويلات مكررة في التسوية. المطلوب حزمة واحدة بملف واحد main.go تقوم بما يلي، بدون أي شبكة فعلية (الطلبات الواردة تُمرَّر كسلاسل نصية جاهزة لدوالك، والمفاتيح ثابتة في نص الملف): ParseMadaPay(body string, signatureHex string, secret string) (PaymentEvent, error) — تتحقق من HMAC للجسم كاملاً، وترفض بخطأ واضح عند عدم التطابق. ParsePayLine(body string, secret string) (PaymentEvent, error) — تحسب التوقيع من الحقول المرتبة أبجدياً (باستثناء حقل sign نفسه)، وتتحقق. هيكل موحّد: go type PaymentEvent struct { &#x20; Provider string // "mada\_pay" | "payline" &#x20; EventID string // معرّف فريد عند كل مزود &#x20; Status string // "succeeded" | "failed" — يجب توحيد الحالات ("SUCCESS","paid","completed" → succeeded) &#x20; AmountCents int64 &#x20; Currency string // ISO 4217 upper-case &#x20; Reference string // رقم طلبنا عند المزود &#x20; OccurredAt time.Time // بصيغة UTC } Dedupe(events \[]PaymentEvent) \[]PaymentEvent — تعيد قائمة بدون تكرار: المفتاح هو Provider + ":" + EventID، وعند التكرار يُبقى أول ظهور فقط مع الحفاظ على الترتيب. أخطاء التحويل: مبالغ سالبة أو صفر تُرفض، عملة غير معروفة تُرفض، OccurredAt بصيغة غير صالحة تُرفض — كلها بأخطاء مميزة القيمة (sentinel errors). بيانات تجريبية مضمّنة في main تُظهر: إشعارا نجاح من المزودين لنفس الطلب (منصوعين يدوياً بمفاتيح ثابتة في الملف)، إشعار payline مكرر، وإشعار بتوقيع تالف — مع طباعة الأحداث الموحدة النهائية. حتمي بالكامل، لا time.Now، لا قراءة من ملفات أو الشبكة، مكتبة قياسية فقط (crypto/hmac, crypto/sha256, encoding/hex, encoding/json, sort, strings, time).

الردودُ الخمسة كما وصلت المحكّمين

الرمزُ هو ما رآه المحكّمان؛ والاسمُ كُشف بعد التحكيم.

بGAIBRA95.5 · 3,267 كلمة
// main.go // Package main normalizes signed payment notifications from the Mada-Pay and // PayLine gateways into one PaymentEvent model and removes retried duplicates. package main import ( &#x09;"crypto/hmac" &#x09;"crypto/sha256" &#x09;"encoding/hex" &#x09;"encoding/json" &#x09;"errors" &#x09;"fmt" &#x09;"sort" &#x09;"strings" &#x09;"time" ) // Provider identifiers stored in PaymentEvent.Provider. const ( &#x09;ProviderMadaPay = "mada\_pay" &#x09;ProviderPayLine = "payline" ) // Canonical statuses stored in PaymentEvent.Status. const ( &#x09;StatusSucceeded = "succeeded" &#x09;StatusFailed = "failed" ) const ( &#x09;madaPaySignaturePrefix = "sha256=" &#x09;payLineSignatureField = "sign" &#x09;payLineValueSeparator = "\&" &#x09;dedupeKeySeparator = ":" &#x09;maxNotificationBytes = 64 \* 1024 ) // PayLine body keys (assumed contract; replace with the provider's documented names). const ( &#x09;payLineFieldEventID = "event\_id" &#x09;payLineFieldState = "state" &#x09;payLineFieldAmount = "amount\_minor" &#x09;payLineFieldCurrency = "currency\_code" &#x09;payLineFieldReference = "order\_no" &#x09;payLineFieldTimestamp = "timestamp" ) // Logical field names used in error messages. const ( &#x09;fieldNameEventID = "event id" &#x09;fieldNameStatus = "status" &#x09;fieldNameAmount = "amount" &#x09;fieldNameCurrency = "currency" &#x09;fieldNameReference = "reference" &#x09;fieldNameOccurredAt = "occurred at" ) // Demo-only secrets, fixed in the file as requested. Production code must load // secrets from a secret store and never commit them. const ( &#x09;demoMadaPaySecret = "demo-mada-pay-secret-not-for-production" &#x09;demoPayLineSecret = "demo-payline-secret-not-for-production" ) // Sentinel errors. Callers branch on them with errors.Is. var ( &#x09;ErrEmptySecret = errors.New("payment: empty signing secret") &#x09;ErrPayloadTooLarge = errors.New("payment: notification body too large") &#x09;ErrMalformedPayload = errors.New("payment: malformed notification payload") &#x09;ErrInvalidSignature = errors.New("payment: signature verification failed") &#x09;ErrMissingField = errors.New("payment: required field missing") &#x09;ErrUnknownStatus = errors.New("payment: unknown payment status") &#x09;ErrInvalidAmount = errors.New("payment: amount must be a positive integer in minor units") &#x09;ErrUnknownCurrency = errors.New("payment: unknown currency") &#x09;ErrInvalidTimestamp = errors.New("payment: invalid occurred-at timestamp") ) // knownCurrencies is the ISO 4217 allow-list accepted by this service. var knownCurrencies = map\[string]struct{}{ &#x09;"SAR": {}, "AED": {}, "BHD": {}, "KWD": {}, "OMR": {}, "QAR": {}, &#x09;"JOD": {}, "EGP": {}, "USD": {}, "EUR": {}, "GBP": {}, } // statusAliases maps lower-cased provider statuses to canonical statuses. var statusAliases = map\[string]string{ &#x09;"success": StatusSucceeded, &#x09;"succeeded": StatusSucceeded, &#x09;"paid": StatusSucceeded, &#x09;"completed": StatusSucceeded, &#x09;"failed": StatusFailed, &#x09;"failure": StatusFailed, &#x09;"declined": StatusFailed, &#x09;"rejected": StatusFailed, } // PaymentEvent is the provider-independent payment notification. type PaymentEvent struct { &#x09;Provider string // "mada\_pay" | "payline" &#x09;EventID string // unique per provider &#x09;Status string // "succeeded" | "failed" &#x09;AmountCents int64 // minor units, always > 0 &#x09;Currency string // ISO 4217 upper-case &#x09;Reference string // our order number as known to the provider &#x09;OccurredAt time.Time // UTC } // madaPayNotification is the assumed Mada-Pay body shape. type madaPayNotification struct { &#x09;ID string `json:"id"` &#x09;Status string `json:"status"` &#x09;Amount json.Number `json:"amount"` &#x09;Currency string `json:"currency"` &#x09;MerchantReference string `json:"merchant\_reference"` &#x09;CreatedAt string `json:"created\_at"` } // rawNotification carries provider values before validation and normalization. type rawNotification struct { &#x09;Provider string &#x09;EventID string &#x09;Status string &#x09;Amount string &#x09;Currency string &#x09;Reference string &#x09;OccurredAt string } // ParseMadaPay verifies the HMAC-SHA256 of the whole raw body against // signatureHex (with or without the "sha256=" prefix), then normalizes it. // It returns ErrInvalidSignature on mismatch and other sentinels on bad data. func ParseMadaPay(body string, signatureHex string, secret string) (PaymentEvent, error) { &#x09;if err := validateNotificationInput(body, secret); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;if err := verifyMadaPaySignature(body, signatureHex, secret); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;var notification madaPayNotification &#x09;if err := json.Unmarshal(\[]byte(body), \&notification); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("%w: %s: %v", ErrMalformedPayload, ProviderMadaPay, err) &#x09;} &#x09;return normalizeNotification(rawNotification{ &#x09; Provider: ProviderMadaPay, &#x09; EventID: notification.ID, &#x09; Status: notification.Status, &#x09; Amount: notification.Amount.String(), &#x09; Currency: notification.Currency, &#x09; Reference: notification.MerchantReference, &#x09; OccurredAt: notification.CreatedAt, &#x09;}) } // ParsePayLine verifies the "sign" field, computed as HMAC-SHA256 over the // values of all other fields sorted by key and joined with "\&", then normalizes it. // Values containing "\&", null values and nested objects/arrays are rejected // with ErrMalformedPayload because they make the signed string ambiguous. func ParsePayLine(body string, secret string) (PaymentEvent, error) { &#x09;if err := validateNotificationInput(body, secret); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;fields, err := decodeFlatJSONObject(ProviderPayLine, body) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;if err := verifyPayLineSignature(fields, secret); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;raw, err := payLineRawNotification(fields) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return normalizeNotification(raw) } // Dedupe returns events without duplicates keyed by Provider + ":" + EventID, // keeping the first occurrence and the original order. It never returns nil. func Dedupe(events \[]PaymentEvent) \[]PaymentEvent { &#x09;unique := make(\[]PaymentEvent, 0, len(events)) &#x09;seen := make(map\[string]struct{}, len(events)) &#x09;for \_, event := range events { &#x09; key := dedupeKey(event) &#x09; if \_, duplicate := seen\[key]; duplicate { &#x09; continue &#x09; } &#x09; seen\[key] = struct{}{} &#x09; unique = append(unique, event) &#x09;} &#x09;return unique } func dedupeKey(event PaymentEvent) string { &#x09;return event.Provider + dedupeKeySeparator + event.EventID } func validateNotificationInput(body string, secret string) error { &#x09;if secret == "" { &#x09; return ErrEmptySecret &#x09;} &#x09;if len(body) > maxNotificationBytes { &#x09; return fmt.Errorf("%w: %d bytes exceeds %d", ErrPayloadTooLarge, len(body), maxNotificationBytes) &#x09;} &#x09;if strings.TrimSpace(body) == "" { &#x09; return fmt.Errorf("%w: empty body", ErrMalformedPayload) &#x09;} &#x09;return nil } func computeHMACSHA256(message \[]byte, secret string) \[]byte { &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;// hash.Hash.Write is documented to never return an error. &#x09;mac.Write(message) &#x09;return mac.Sum(nil) } func decodeHexDigest(provider string, signatureHex string) (\[]byte, error) { &#x09;digest, err := hex.DecodeString(strings.TrimSpace(signatureHex)) &#x09;if err != nil || len(digest) != sha256.Size { &#x09; return nil, fmt.Errorf("%w: %s: signature is not a %d-byte hex digest", ErrInvalidSignature, provider, sha256.Size) &#x09;} &#x09;return digest, nil } func verifyMadaPaySignature(body string, signatureHeader string, secret string) error { &#x09;signatureHex := strings.TrimPrefix(strings.TrimSpace(signatureHeader), madaPaySignaturePrefix) &#x09;provided, err := decodeHexDigest(ProviderMadaPay, signatureHex) &#x09;if err != nil { &#x09; return err &#x09;} &#x09;if !hmac.Equal(provided, computeHMACSHA256(\[]byte(body), secret)) { &#x09; return fmt.Errorf("%w: %s: digest mismatch", ErrInvalidSignature, ProviderMadaPay) &#x09;} &#x09;return nil } func decodeFlatJSONObject(provider string, body string) (map\[string]json.RawMessage, error) { &#x09;var fields map\[string]json.RawMessage &#x09;if err := json.Unmarshal(\[]byte(body), \&fields); err != nil { &#x09; return nil, fmt.Errorf("%w: %s: %v", ErrMalformedPayload, provider, err) &#x09;} &#x09;if fields == nil { &#x09; return nil, fmt.Errorf("%w: %s: body must be a JSON object", ErrMalformedPayload, provider) &#x09;} &#x09;return fields, nil } func verifyPayLineSignature(fields map\[string]json.RawMessage, secret string) error { &#x09;provided, err := payLineProvidedSignature(fields) &#x09;if err != nil { &#x09; return err &#x09;} &#x09;canonical, err := payLineCanonicalString(fields) &#x09;if err != nil { &#x09; return err &#x09;} &#x09;if !hmac.Equal(provided, computeHMACSHA256(\[]byte(canonical), secret)) { &#x09; return fmt.Errorf("%w: %s: digest mismatch", ErrInvalidSignature, ProviderPayLine) &#x09;} &#x09;return nil } func payLineProvidedSignature(fields map\[string]json.RawMessage) (\[]byte, error) { &#x09;raw, ok := fields\[payLineSignatureField] &#x09;if !ok { &#x09; return nil, fmt.Errorf("%w: %s: missing %q field", ErrInvalidSignature, ProviderPayLine, payLineSignatureField) &#x09;} &#x09;var signatureHex string &#x09;if err := json.Unmarshal(raw, \&signatureHex); err != nil { &#x09; return nil, fmt.Errorf("%w: %s: %q must be a JSON string", ErrInvalidSignature, ProviderPayLine, payLineSignatureField) &#x09;} &#x09;return decodeHexDigest(ProviderPayLine, signatureHex) } // payLineCanonicalString builds value1\&value2\&... ordered by byte-wise key order, excluding "sign". func payLineCanonicalString(fields map\[string]json.RawMessage) (string, error) { &#x09;keys := make(\[]string, 0, len(fields)) &#x09;for key := range fields { &#x09; if key != payLineSignatureField { &#x09; keys = append(keys, key) &#x09; } &#x09;} &#x09;sort.Strings(keys) &#x09;values := make(\[]string, 0, len(keys)) &#x09;for \_, key := range keys { &#x09; value, err := canonicalPayLineValue(fields\[key]) &#x09; if err != nil { &#x09; return "", fmt.Errorf("%s field %q: %w", ProviderPayLine, key, err) &#x09; } &#x09; if strings.Contains(value, payLineValueSeparator) { &#x09; return "", fmt.Errorf("%w: %s field %q contains the separator %q", ErrMalformedPayload, ProviderPayLine, key, payLineValueSeparator) &#x09; } &#x09; values = append(values, value) &#x09;} &#x09;return strings.Join(values, payLineValueSeparator), nil } // canonicalPayLineValue returns strings unquoted and numbers/booleans as their raw literal. func canonicalPayLineValue(raw json.RawMessage) (string, error) { &#x09;literal := strings.TrimSpace(string(raw)) &#x09;switch { &#x09;case literal == "" || literal == "null": &#x09; return "", fmt.Errorf("%w: null or empty value cannot be signed", ErrMalformedPayload) &#x09;case literal\[0] == '"': &#x09; var value string &#x09; if err := json.Unmarshal(raw, \&value); err != nil { &#x09; return "", fmt.Errorf("%w: %v", ErrMalformedPayload, err) &#x09; } &#x09; return value, nil &#x09;case literal\[0] == '{' || literal\[0] == '\[': &#x09; return "", fmt.Errorf("%w: nested objects and arrays cannot be signed", ErrMalformedPayload) &#x09;default: &#x09; return literal, nil &#x09;} } func payLineFieldValue(fields map\[string]json.RawMessage, key string) (string, error) { &#x09;raw, ok := fields\[key] &#x09;if !ok { &#x09; return "", nil &#x09;} &#x09;return canonicalPayLineValue(raw) } func payLineRawNotification(fields map\[string]json.RawMessage) (rawNotification, error) { &#x09;keys := \[]string{payLineFieldEventID, payLineFieldState, payLineFieldAmount, &#x09; payLineFieldCurrency, payLineFieldReference, payLineFieldTimestamp} &#x09;values := make(map\[string]string, len(keys)) &#x09;for \_, key := range keys { &#x09; value, err := payLineFieldValue(fields, key) &#x09; if err != nil { &#x09; return rawNotification{}, fmt.Errorf("%s field %q: %w", ProviderPayLine, key, err) &#x09; } &#x09; values\[key] = value &#x09;} &#x09;return rawNotification{ &#x09; Provider: ProviderPayLine, &#x09; EventID: values\[payLineFieldEventID], &#x09; Status: values\[payLineFieldState], &#x09; Amount: values\[payLineFieldAmount], &#x09; Currency: values\[payLineFieldCurrency], &#x09; Reference: values\[payLineFieldReference], &#x09; OccurredAt: values\[payLineFieldTimestamp], &#x09;}, nil } func normalizeNotification(raw rawNotification) (PaymentEvent, error) { &#x09;eventID, err := requireField(raw.Provider, fieldNameEventID, raw.EventID) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;status, err := normalizeStatus(raw.Provider, raw.Status) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;amount, err := normalizeAmount(raw.Provider, raw.Amount) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;currency, err := normalizeCurrency(raw.Provider, raw.Currency) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;reference, err := requireField(raw.Provider, fieldNameReference, raw.Reference) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;occurredAt, err := normalizeTimestamp(raw.Provider, raw.OccurredAt) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return PaymentEvent{ &#x09; Provider: raw.Provider, &#x09; EventID: eventID, &#x09; Status: status, &#x09; AmountCents: amount, &#x09; Currency: currency, &#x09; Reference: reference, &#x09; OccurredAt: occurredAt, &#x09;}, nil } func requireField(provider string, fieldName string, value string) (string, error) { &#x09;trimmed := strings.TrimSpace(value) &#x09;if trimmed == "" { &#x09; return "", fmt.Errorf("%w: %s: %s", ErrMissingField, provider, fieldName) &#x09;} &#x09;return trimmed, nil } func normalizeStatus(provider string, rawStatus string) (string, error) { &#x09;trimmed, err := requireField(provider, fieldNameStatus, rawStatus) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;status, ok := statusAliases\[strings.ToLower(trimmed)] &#x09;if !ok { &#x09; return "", fmt.Errorf("%w: %s: %q", ErrUnknownStatus, provider, trimmed) &#x09;} &#x09;return status, nil } func normalizeAmount(provider string, rawAmount string) (int64, error) { &#x09;trimmed, err := requireField(provider, fieldNameAmount, rawAmount) &#x09;if err != nil { &#x09; return 0, err &#x09;} &#x09;amount, err := json.Number(trimmed).Int64() &#x09;if err != nil { &#x09; return 0, fmt.Errorf("%w: %s: %q is not an integer in minor units", ErrInvalidAmount, provider, trimmed) &#x09;} &#x09;if amount <= 0 { &#x09; return 0, fmt.Errorf("%w: %s: %d", ErrInvalidAmount, provider, amount) &#x09;} &#x09;return amount, nil } func normalizeCurrency(provider string, rawCurrency string) (string, error) { &#x09;trimmed, err := requireField(provider, fieldNameCurrency, rawCurrency) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;currency := strings.ToUpper(trimmed) &#x09;if \_, ok := knownCurrencies\[currency]; !ok { &#x09; return "", fmt.Errorf("%w: %s: %q", ErrUnknownCurrency, provider, trimmed) &#x09;} &#x09;return currency, nil } func normalizeTimestamp(provider string, rawTimestamp string) (time.Time, error) { &#x09;trimmed, err := requireField(provider, fieldNameOccurredAt, rawTimestamp) &#x09;if err != nil { &#x09; return time.Time{}, err &#x09;} &#x09;parsed, err := time.Parse(time.RFC3339, trimmed) &#x09;if err != nil { &#x09; return time.Time{}, fmt.Errorf("%w: %s: %q is not RFC 3339 with offset", ErrInvalidTimestamp, provider, trimmed) &#x09;} &#x09;return parsed.UTC(), nil } // madaPaySignatureHeader builds the X-Mada-Signature value; used for demo fixtures. func madaPaySignatureHeader(body string, secret string) string { &#x09;return madaPaySignaturePrefix + hex.EncodeToString(computeHMACSHA256(\[]byte(body), secret)) } // signPayLineBody adds a "sign" field using the same canonical rule as ParsePayLine. func signPayLineBody(body string, secret string) (string, error) { &#x09;if err := validateNotificationInput(body, secret); err != nil { &#x09; return "", err &#x09;} &#x09;fields, err := decodeFlatJSONObject(ProviderPayLine, body) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;delete(fields, payLineSignatureField) &#x09;canonical, err := payLineCanonicalString(fields) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;encodedSignature, err := json.Marshal(hex.EncodeToString(computeHMACSHA256(\[]byte(canonical), secret))) &#x09;if err != nil { &#x09; return "", fmt.Errorf("encode payline signature: %w", err) &#x09;} &#x09;fields\[payLineSignatureField] = json.RawMessage(encodedSignature) &#x09;signed, err := json.Marshal(fields) &#x09;if err != nil { &#x09; return "", fmt.Errorf("encode signed payline body: %w", err) &#x09;} &#x09;return string(signed), nil } type demoCase struct { &#x09;label string &#x09;provider string &#x09;body string &#x09;madaSignature string &#x09;wantErr error } const ( &#x09;demoExpectedAccepted = 4 &#x09;demoExpectedUnique = 3 &#x09;demoExpectedRejected = 5 ) func buildDemoCases() (\[]demoCase, error) { &#x09;const madaSuccess = `{"id":"mp\_evt\_1001","status":"SUCCESS","amount":15000,"currency":"sar","merchant\_reference":"ORD-2026-0042","created\_at":"2026-10-28T21:15:30+03:00"}` &#x09;const madaDeclined = `{"id":"mp\_evt\_1004","status":"declined","amount":4200,"currency":"SAR","merchant\_reference":"ORD-2026-0044","created\_at":"2026-10-28T23:05:00+03:00"}` &#x09;const madaNegative = `{"id":"mp\_evt\_1002","status":"paid","amount":-500,"currency":"SAR","merchant\_reference":"ORD-2026-0045","created\_at":"2026-10-28T22:00:00+03:00"}` &#x09;const madaBadTime = `{"id":"mp\_evt\_1003","status":"completed","amount":9900,"currency":"SAR","merchant\_reference":"ORD-2026-0043","created\_at":"28/10/2026 21:15"}` &#x09;madaSuccessSignature := madaPaySignatureHeader(madaSuccess, demoMadaPaySecret) &#x09;madaTampered := strings.Replace(madaSuccess, `"amount":15000`, `"amount":1500000`, 1) &#x09;payLineSuccess, err := signPayLineBody(`{"event\_id":"pl\_7f3a","state":"completed","amount\_minor":15000,"currency\_code":"SAR","order\_no":"ORD-2026-0042","timestamp":"2026-10-28T18:16:02Z"}`, demoPayLineSecret) &#x09;if err != nil { &#x09; return nil, err &#x09;} &#x09;payLineUnknownCurrency, err := signPayLineBody(`{"event\_id":"pl\_7f3b","state":"paid","amount\_minor":7000,"currency\_code":"XYZ","order\_no":"ORD-2026-0046","timestamp":"2026-10-28T19:00:00Z"}`, demoPayLineSecret) &#x09;if err != nil { &#x09; return nil, err &#x09;} &#x09;payLineZeroAmount, err := signPayLineBody(`{"event\_id":"pl\_7f3c","state":"paid","amount\_minor":0,"currency\_code":"SAR","order\_no":"ORD-2026-0047","timestamp":"2026-10-28T19:30:00Z"}`, demoPayLineSecret) &#x09;if err != nil { &#x09; return nil, err &#x09;} &#x09;return \[]demoCase{ &#x09; {label: "mada\_pay success ORD-2026-0042", provider: ProviderMadaPay, body: madaSuccess, madaSignature: madaSuccessSignature}, &#x09; {label: "payline success ORD-2026-0042", provider: ProviderPayLine, body: payLineSuccess}, &#x09; {label: "payline retry (same event)", provider: ProviderPayLine, body: payLineSuccess}, &#x09; {label: "mada\_pay declined ORD-2026-0044", provider: ProviderMadaPay, body: madaDeclined, madaSignature: madaPaySignatureHeader(madaDeclined, demoMadaPaySecret)}, &#x09; {label: "mada\_pay tampered amount", provider: ProviderMadaPay, body: madaTampered, madaSignature: madaSuccessSignature, wantErr: ErrInvalidSignature}, &#x09; {label: "mada\_pay negative amount", provider: ProviderMadaPay, body: madaNegative, madaSignature: madaPaySignatureHeader(madaNegative, demoMadaPaySecret), wantErr: ErrInvalidAmount}, &#x09; {label: "mada\_pay bad timestamp", provider: ProviderMadaPay, body: madaBadTime, madaSignature: madaPaySignatureHeader(madaBadTime, demoMadaPaySecret), wantErr: ErrInvalidTimestamp}, &#x09; {label: "payline unknown currency", provider: ProviderPayLine, body: payLineUnknownCurrency, wantErr: ErrUnknownCurrency}, &#x09; {label: "payline zero amount", provider: ProviderPayLine, body: payLineZeroAmount, wantErr: ErrInvalidAmount}, &#x09;}, nil } func parseDemoCase(c demoCase) (PaymentEvent, error) { &#x09;switch c.provider { &#x09;case ProviderMadaPay: &#x09; return ParseMadaPay(c.body, c.madaSignature, demoMadaPaySecret) &#x09;case ProviderPayLine: &#x09; return ParsePayLine(c.body, demoPayLineSecret) &#x09;default: &#x09; return PaymentEvent{}, fmt.Errorf("demo: unsupported provider %q", c.provider) &#x09;} } func checkDemoResult(accepted \[]PaymentEvent, unique \[]PaymentEvent, rejected int) error { &#x09;if len(accepted) != demoExpectedAccepted || len(unique) != demoExpectedUnique || rejected != demoExpectedRejected { &#x09; return fmt.Errorf("demo check failed: accepted=%d unique=%d rejected=%d", len(accepted), len(unique), rejected) &#x09;} &#x09;first := unique\[0] &#x09;wantTime := time.Date(2026, time.October, 28, 18, 15, 30, 0, time.UTC) &#x09;if first.Provider != ProviderMadaPay || first.Status != StatusSucceeded || first.Currency != "SAR" || &#x09; first.AmountCents != 15000 || !first.OccurredAt.Equal(wantTime) { &#x09; return fmt.Errorf("demo check failed: unexpected first event %+v", first) &#x09;} &#x09;if unique\[1].Provider != ProviderPayLine || unique\[1].EventID != "pl\_7f3a" { &#x09; return fmt.Errorf("demo check failed: unexpected second event %+v", unique\[1]) &#x09;} &#x09;if unique\[2].Status != StatusFailed { &#x09; return fmt.Errorf("demo check failed: declined was not normalized to failed: %+v", unique\[2]) &#x09;} &#x09;return nil } func run() error { &#x09;cases, err := buildDemoCases() &#x09;if err != nil { &#x09; return fmt.Errorf("build demo cases: %w", err) &#x09;} &#x09;accepted := make(\[]PaymentEvent, 0, len(cases)) &#x09;rejected := 0 &#x09;for \_, c := range cases { &#x09; event, parseErr := parseDemoCase(c) &#x09; if c.wantErr != nil { &#x09; if !errors.Is(parseErr, c.wantErr) { &#x09; return fmt.Errorf("demo check failed for %s: want %v, got %v", c.label, c.wantErr, parseErr) &#x09; } &#x09; rejected++ &#x09; fmt.Printf("REJECTED %-32s %v\\n", c.label, parseErr) &#x09; continue &#x09; } &#x09; if parseErr != nil { &#x09; return fmt.Errorf("demo check failed for %s: %w", c.label, parseErr) &#x09; } &#x09; accepted = append(accepted, event) &#x09; fmt.Printf("ACCEPTED %-32s %s\\n", c.label, dedupeKey(event)) &#x09;} &#x09;unique := Dedupe(accepted) &#x09;if err := checkDemoResult(accepted, unique, rejected); err != nil { &#x09; return err &#x09;} &#x09;fmt.Println("Unified payment events:") &#x09;for \_, e := range unique { &#x09; fmt.Printf("%-8s %-11s %-9s %7d %s %-13s %s\\n", &#x09; e.Provider, e.EventID, e.Status, e.AmountCents, e.Currency, e.Reference, e.OccurredAt.Format(time.RFC3339)) &#x09;} &#x09;return nil } func main() { &#x09;if err := run(); err != nil { &#x09; panic(err) &#x09;} } الشرح أخطر ما في مخطّط «باي-لاين» ليس اختلافَ أسماء الحقول، بل أنّ التوقيع يغطّي القيم وحدها مفصولةً بـ \&. معنى ذلك أنّ مهاجماً يستطيع نقلَ الحدّ بين حقلين متجاورين دون أن يتغيّر النصّ الموقَّع. فلو كانت قيمةُ حقلٍ x\&y وما يليه z، فإعادةُ كتابتهما x وy\&z تُبقي السلسلة نفسها x\&y\&z، ويمرّ التوقيعُ سليماً على جسمٍ مختلف. لهذا يرفض payLineCanonicalString أيَّ قيمةٍ تحوي الفاصل. وما دامت القيمُ خاليةً منه، فعددُ المقاطع يساوي عددَ المفاتيح، ولا يمكن إضافةُ حقلٍ أو حذفُه دون كسر التوقيع. وللسبب ذاته تُقرأ حقولُ «باي-لاين» من الخريطة الموقَّعة نفسِها بمطابقةٍ حرفيّةٍ للمفاتيح، لا بفكّ JSON إلى struct. فـ encoding/json يطابق أسماءَ الحقول دون تمييزٍ لحالة الأحرف، وهذا يفتح فجوةً بين ما وُقِّع وما قُرئ. الثمنُ واضح: مرجعُ طلبٍ مشروعٌ يحتوي \& سيُرفض بـ ErrMalformedPayload. وأراه ثمناً مقبولاً مقابل إغلاق ثغرةٍ تمسّ المبالغ. في «مدى-بي» الأمرُ أبسط، لأنّ التوقيع يغطّي الجسمَ الخامَ كاملاً، فيُتحقَّق منه قبل أيّ تحليل. لا تُلمَس بيانةٌ غيرُ موثَّقة، وتُقارَن البصمتان بـ hmac.Equal في زمنٍ ثابتٍ يمنع تسريبَ التوقيع عبر التوقيت. تقبل الدالّةُ التوقيعَ بالبادئة sha256= أو بدونها، ويُرفض كلُّ ما ليس 32 بايتاً بالضبط. ويترتّب على ذلك شرطٌ عمليّ: يجب أن يصل إلى ParseMadaPay الجسمُ بالبايتات التي وصلت عبر الشبكة. أيُّ middleware يعيد تنسيقَ الـ JSON سيُفشل كلَّ التواقيع. بقيّةُ القرارات تتبع المنطقَ نفسَه. الأخطاءُ sentinel تُغلَّف بـ %w، فيتفرّع عليها المستدعي بـ errors.Is ويقرّر: توقيعٌ تالفٌ يُردّ عليه بـ 401 ويُسجَّل كمحاولة اختراق، أمّا عملةٌ مجهولةٌ فتحتاج تدخّلاً بشريّاً. الحالاتُ غيرُ المعروفة مثل pending أو refunded تُرفض بـ ErrUnknownStatus ولا تُخمَّن، لأنّ تحويلَ حالةٍ مجهولةٍ إلى succeeded أسوأُ من رفضها. الوقتُ يُقبل بصيغة RFC 3339 مع إزاحةٍ صريحة فقط، ثمّ يُحوَّل إلى UTC، فتصير 21:15:30+03:00 في المثال 18:15:30Z. أمّا Dedupe فتستعمل map بسعةٍ محجوزةٍ مسبقاً، فتعمل بزمنٍ خطّيّ دون إعادة تخصيص. والدوالُّ كلُّها بلا حالةٍ مشتركة، فهي آمنةٌ للاستدعاء المتزامن من عدّة goroutines. هناك عقدٌ لم أره فاضطررتُ لافتراضه، وهذه حدوده: الحقل الموحّد «مدى-بي» (مفترَض) «باي-لاين» (مفترَض) EventID id event\_id Status status state AmountCents amount (هللات صحيحة) amount\_minor Currency currency currency\_code Reference merchant\_reference order\_no OccurredAt created\_at timestamp لم تذكر أسماءَ الحقول الفعليّة. افترضتُ أيضاً أنّ المبالغ تصل بالوحدة الصغرى، وأنّ ترتيبَ المفاتيح بايتيٌّ تصاعديّ، وأنّ قيمةَ null لا تُوقَّع. أرسل لي مثالاً حقيقيّاً موقَّعاً من كلّ بوابة، وسأضبط الثوابتَ وقاعدةَ التوقيع عليه هنا. ولو تبيّن أنّ «باي-لاين» ترسل المبلغ عشريّاً مثل "150.00"، فسيلزم جدولُ منازلٍ عشريّةٍ لكلّ عملة، لأنّ KWD وBHD تستعملان ثلاثَ منازل. وأهمُّ تنبيهٍ يتعلّق بالمشكلة التي بدأتَ منها: مهمّ Dedupe تزيل التكرار داخل الدفعة الواحدة في الذاكرة فقط؛ لن توقف التحويلات المكرّرة في التسوية إن وصلت الإعادةُ بعد إعادة تشغيل الخدمة أو إلى نسخةٍ أخرى منها. الحلُّ الجذريّ قيدُ تفرّدٍ في قاعدة البيانات على (provider, event\_id) مع إدراجٍ يتجاهل التعارض، ثمّ الردّ بـ 200 حتى على المكرّر. لم أُدرجه لأنّ طلبك ينصّ على «بدون أي شبكة فعلية» و«لا قراءة من ملفات». وللسبب نفسه («لا time.Now») لا يوجد فحصٌ لحداثة الإشعار ضدّ إعادة الإرسال الخبيثة. بعد هذا التنبيه تبقى ملاحظةٌ تخصّ التسوية تحديداً. نجاحُ الطلب ORD-2026-0042 من البوابتين معاً يُنتج حدثين يحتفظ بهما Dedupe كلاهما، لأنّ المفتاح الذي حدّدتَه Provider:EventID يختلف بينهما. هذا ليس تكراراً تقنيّاً، بل احتمالُ أنّ العميل دفع مرّتين فعلاً، والصوابُ أن يُعالَج كاستردادٍ لا أن يُحذف بصمت. المفتاحان ثابتان في الملف لأنّك طلبتَ ذلك للعرض. وأضفتُ fmt وerrors إلى القائمة التي ذكرتَها، لأنّ الطباعةَ وerrors.Is تحتاجانهما، وكلاهما من المكتبة القياسيّة. لم أشغّل هذا الكود. لكنّ run لا تكتفي بالطباعة. فهي تتحقّق بنفسها من النتيجة، وتنهي البرنامج بـ panic إن اختلف شيءٌ عمّا يلي: كلُّ حالةٍ مرفوضة رُفضت بخطئها المحدّد عبر errors.Is. عددُ الأحداث المقبولة 4، والفريدة بعد Dedupe 3، والمرفوضة 5. الحدثُ الأوّل من «مدى-بي» بحالة succeeded وعملة SAR ومبلغ 15000 ووقت 2026-10-28T18:15:30Z. الثاني هو pl\_7f3a. الثالث تحوّلت حالتُه من declined إلى failed. الدليلُ إذن هو تشغيلُك لأمرٍ واحد، لا كلامي. طريقة الاستخدام شغّل الملف بالأمر go run main.go. تتبّعتُ الكود يدويّاً على البيانات المضمّنة، ولم أشغّله. وفق هذا التتبّع يجب أن يظهر ما يلي: قبول إشعار «مدى-بي» الناجح، ثمّ إشعار «باي-لاين» الناجح، ثمّ إعادته بالمفتاح نفسه payline:pl\_7f3a. قبول إشعار الرفض mp\_evt\_1004. رفض خمس حالات بالترتيب: التوقيع التالف بـ ErrInvalidSignature، والمبلغ السالب بـ ErrInvalidAmount، والوقت 28/10/2026 21:15 بـ ErrInvalidTimestamp، والعملة XYZ بـ ErrUnknownCurrency، والمبلغ الصفريّ بـ ErrInvalidAmount. ثمّ تُطبع الأحداث الموحّدة: Unified payment events: mada\_pay mp\_evt\_1001 succeeded 15000 SAR ORD-2026-0042 2026-10-28T18:15:30Z payline pl\_7f3a succeeded 15000 SAR ORD-2026-0042 2026-10-28T18:16:02Z mada\_pay mp\_evt\_1004 failed 4200 SAR ORD-2026-0044 2026-10-28T20:05:00Z إعادةُ «باي-لاين» اختفت كما يجب. لكنّ السطرين الأوّلين لا يزالان يحملان الطلب ORD-2026-0042 نفسه. هذه هي حالة الدفع المزدوج التي أشرتُ إليها، وتحتاج معالجةً منفصلة. عند الدمج في الـ handler الحقيقيّ: مرّر الجسمَ الخام كما قرأته من الطلب إلى ParseMadaPay أو ParsePayLine، بعد قصّه بـ http.MaxBytesReader عند الحدّ نفسه 64 \* 1024. إن طابق الخطأ ErrInvalidSignature فردّ بـ 401. إن طابق ErrUnknownCurrency أو ErrUnknownStatus فردّ بـ 422 وأرسل الحالة إلى مراجعةٍ بشريّة. خطوتك التالية: أنشئ قيد التفرّد على (provider, event\_id) في جدول الإشعارات، واجعل الإدراج يتجاهل التعارض ثمّ يردّ بـ 200. بذلك تتوقّف التحويلاتُ المكرّرة في التسوية حتى عبر إعادة التشغيل وتعدّد النسخ. وأرسل لي عيّنةً موقّعةً حقيقيّةً من كلّ بوابة لأضبط عليها أسماءَ الحقول وقاعدةَ التوقيع المفترضة.
هـClaude Sonnet90.5 · 1,835 كلمة
سأكتب main.go ثم أشغّله للتأكد من المخرجات. main.go جاهز، وقد شغّلته بعد فحص gofmt وgo vet (نظيفان). النتيجة حتمية: من 3 إشعارات مقبولة بقي حدثان فقط، وأُسقط الإشعار المكرر من باي-لاين، ورُفض الإشعار ذو التوقيع التالف بـ ErrBadSignature. التوقيعان: مدى-بي يُتحقق منه على الجسم الخام كاملاً قبل أي تحليل JSON، وتُقبل ترويسة sha256=<hex> أو الـ hex وحده. باي-لاين يُحسب توقيعه من قيم الحقول (عدا sign) مرتبةً حسب اسم الحقل ومفصولة بـ \&، والمقارنة في الحالتين بـ hmac.Equal. افتراضات صغيرة في الحقول (غيّرها إن كانت وثائق المزودين تقول غير ذلك): مدى-بي: المبلغ بأصغر وحدة (هللة) والوقت بصيغة RFC3339 (يُحوَّل إلى UTC). باي-لاين: المبلغ نص عشري مثل "150.75" يُحوَّل بدون أرقام عائمة، والوقت ts بالثواني. الحالات: SUCCESS/paid/completed/approved تصبح succeeded، وfailed/declined/rejected/error تصبح failed، وأي حالة أخرى تُرفض بـ ErrUnknownStatus. الأخطاء المميزة: ErrBadSignature وErrInvalidAmount وErrUnknownCurrency وErrInvalidTime، إضافةً إلى ErrMalformedBody وErrMissingField وErrUnknownStatus. كلها تُغلَّف بسياق ويُفحص بها عبر errors.Is. لاحظ أن المخرجات تُظهر حدثي نجاح لنفس الطلب ORD-2026-1042، واحداً من كل مزود. Dedupe بمفتاح Provider:EventID لا يدمجهما لأن المعرّفين مختلفان، وهذا ما طلبته. إن كان هذا ما يسبب التحويلات المكررة في التسوية، فتحتاج خطوة منفصلة تُجمّع بحسب Reference. الملف: /mnt/user-data/uploads/main.go Main Code ·GO // حزمة واحدة بملف واحد: تحقق وتوحيد إشعارات الدفع من «مدى-بي» و«باي-لاين» // مع إزالة التكرار الناتج عن إعادة الإرسال (retry). // حتمي بالكامل: لا شبكة، لا ملفات، لا time.Now، مكتبة قياسية فقط. package main import ( &#x09;"crypto/hmac" &#x09;"crypto/sha256" &#x09;"encoding/hex" &#x09;"encoding/json" &#x09;"errors" &#x09;"fmt" &#x09;"sort" &#x09;"strconv" &#x09;"strings" &#x09;"time" ) // --------------------------------------------------------------------------- // الأخطاء المميزة (sentinel errors) // --------------------------------------------------------------------------- var ( &#x09;ErrBadSignature = errors.New("توقيع الإشعار غير مطابق") &#x09;ErrMalformedBody = errors.New("جسم الإشعار غير صالح") &#x09;ErrMissingField = errors.New("حقل مطلوب مفقود") &#x09;ErrInvalidAmount = errors.New("المبلغ غير صالح (يجب أن يكون أكبر من صفر)") &#x09;ErrUnknownCurrency = errors.New("عملة غير معروفة") &#x09;ErrInvalidTime = errors.New("صيغة وقت الحدث غير صالحة") &#x09;ErrUnknownStatus = errors.New("حالة دفع غير معروفة") ) // --------------------------------------------------------------------------- // الهيكل الموحّد // --------------------------------------------------------------------------- const ( &#x09;ProviderMadaPay = "mada\_pay" &#x09;ProviderPayLine = "payline" &#x09;StatusSucceeded = "succeeded" &#x09;StatusFailed = "failed" ) type PaymentEvent struct { &#x09;Provider string // "mada\_pay" | "payline" &#x09;EventID string // معرّف فريد عند كل مزود &#x09;Status string // "succeeded" | "failed" &#x09;AmountCents int64 // بأصغر وحدة للعملة (هللة/سنت) &#x09;Currency string // ISO 4217 upper-case &#x09;Reference string // رقم طلبنا عند المزود &#x09;OccurredAt time.Time // UTC } // --------------------------------------------------------------------------- // أدوات التوحيد والتحقق المشتركة // --------------------------------------------------------------------------- // العملات المعتمدة (جميعها بخانتين عشريتين). var knownCurrencies = map\[string]bool{ &#x09;"SAR": true, "AED": true, "USD": true, "EUR": true, &#x09;"GBP": true, "QAR": true, "EGP": true, } var statusAliases = map\[string]string{ &#x09;"success": StatusSucceeded, &#x09;"succeeded": StatusSucceeded, &#x09;"paid": StatusSucceeded, &#x09;"completed": StatusSucceeded, &#x09;"approved": StatusSucceeded, &#x09;"failed": StatusFailed, &#x09;"failure": StatusFailed, &#x09;"declined": StatusFailed, &#x09;"rejected": StatusFailed, &#x09;"error": StatusFailed, } func normalizeStatus(raw string) (string, error) { &#x09;s, ok := statusAliases\[strings.ToLower(strings.TrimSpace(raw))] &#x09;if !ok { &#x09; return "", fmt.Errorf("%w: %q", ErrUnknownStatus, raw) &#x09;} &#x09;return s, nil } func normalizeCurrency(raw string) (string, error) { &#x09;c := strings.ToUpper(strings.TrimSpace(raw)) &#x09;if !knownCurrencies\[c] { &#x09; return "", fmt.Errorf("%w: %q", ErrUnknownCurrency, raw) &#x09;} &#x09;return c, nil } // verifyHMAC يقارن HMAC-SHA256 للرسالة بالتوقيع المستلم (hex، مع بادئة sha256= اختيارية) // بمقارنة ثابتة الزمن. func verifyHMAC(message, secret, gotHex string) error { &#x09;gotHex = strings.TrimSpace(gotHex) &#x09;if len(gotHex) >= 7 \&\& strings.EqualFold(gotHex\[:7], "sha256=") { &#x09; gotHex = gotHex\[7:] &#x09;} &#x09;if gotHex == "" { &#x09; return fmt.Errorf("%w: التوقيع مفقود", ErrBadSignature) &#x09;} &#x09;got, err := hex.DecodeString(gotHex) &#x09;if err != nil { &#x09; return fmt.Errorf("%w: التوقيع ليس hex صالحاً", ErrBadSignature) &#x09;} &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(message)) &#x09;if !hmac.Equal(got, mac.Sum(nil)) { &#x09; return ErrBadSignature &#x09;} &#x09;return nil } // parseDecimalCents يحوّل نصاً عشرياً مثل "150.75" إلى 15075 بدون أرقام عائمة. func parseDecimalCents(s string) (int64, error) { &#x09;s = strings.TrimSpace(s) &#x09;whole, frac, \_ := strings.Cut(s, ".") &#x09;if whole == "" || len(whole) > 15 || len(frac) > 2 { &#x09; return 0, fmt.Errorf("%w: %q", ErrInvalidAmount, s) &#x09;} &#x09;for \_, part := range \[]string{whole, frac} { &#x09; for \_, r := range part { &#x09; if r < '0' || r > '9' { &#x09; return 0, fmt.Errorf("%w: %q", ErrInvalidAmount, s) &#x09; } &#x09; } &#x09;} &#x09;for len(frac) < 2 { &#x09; frac += "0" &#x09;} &#x09;w, err1 := strconv.ParseInt(whole, 10, 64) &#x09;f, err2 := strconv.ParseInt(frac, 10, 64) &#x09;if err1 != nil || err2 != nil { &#x09; return 0, fmt.Errorf("%w: %q", ErrInvalidAmount, s) &#x09;} &#x09;cents := w\*100 + f &#x09;if cents <= 0 { &#x09; return 0, fmt.Errorf("%w: %q", ErrInvalidAmount, s) &#x09;} &#x09;return cents, nil } // --------------------------------------------------------------------------- // مدى-بي // الجسم JSON، والتوقيع في الترويسة X-Mada-Signature بصيغة sha256=<hex> // --------------------------------------------------------------------------- type madaPayload struct { &#x09;EventID string `json:"event\_id"` &#x09;State string `json:"state"` &#x09;AmountMinor json.Number `json:"amount\_minor"` // بأصغر وحدة (هللة) &#x09;Currency string `json:"currency"` &#x09;MerchantRef string `json:"merchant\_ref"` &#x09;CreatedAt string `json:"created\_at"` // RFC 3339 (قد يحمل إزاحة +03:00) } func ParseMadaPay(body string, signatureHex string, secret string) (PaymentEvent, error) { &#x09;// التحقق من الجسم كاملاً قبل أي تحليل له. &#x09;if err := verifyHMAC(body, secret, signatureHex); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w", err) &#x09;} &#x09;var p madaPayload &#x09;dec := json.NewDecoder(strings.NewReader(body)) &#x09;dec.UseNumber() &#x09;if err := dec.Decode(\&p); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w: %v", ErrMalformedBody, err) &#x09;} &#x09;if p.EventID == "" { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w: event\_id", ErrMissingField) &#x09;} &#x09;if p.MerchantRef == "" { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w: merchant\_ref", ErrMissingField) &#x09;} &#x09;status, err := normalizeStatus(p.State) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w", err) &#x09;} &#x09;cents, err := strconv.ParseInt(p.AmountMinor.String(), 10, 64) &#x09;if err != nil || cents <= 0 { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w: %q", ErrInvalidAmount, p.AmountMinor.String()) &#x09;} &#x09;cur, err := normalizeCurrency(p.Currency) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w", err) &#x09;} &#x09;t, err := time.Parse(time.RFC3339, p.CreatedAt) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("mada\_pay: %w: %q", ErrInvalidTime, p.CreatedAt) &#x09;} &#x09;return PaymentEvent{ &#x09; Provider: ProviderMadaPay, &#x09; EventID: p.EventID, &#x09; Status: status, &#x09; AmountCents: cents, &#x09; Currency: cur, &#x09; Reference: p.MerchantRef, &#x09; OccurredAt: t.UTC(), &#x09;}, nil } // --------------------------------------------------------------------------- // باي-لاين // التوقيع في الحقل sign داخل الجسم، وهو HMAC-SHA256 لقيم الحقول // (عدا sign) مرتبةً أبجدياً حسب اسم الحقل، ومفصولة بـ "\&". hex بأحرف صغيرة. // --------------------------------------------------------------------------- // payLineSigningString يبني السلسلة الموقَّعة من الحقول بعد استبعاد sign. func payLineSigningString(fields map\[string]string) string { &#x09;keys := make(\[]string, 0, len(fields)) &#x09;for k := range fields { &#x09; if k != "sign" { &#x09; keys = append(keys, k) &#x09; } &#x09;} &#x09;sort.Strings(keys) &#x09;vals := make(\[]string, len(keys)) &#x09;for i, k := range keys { &#x09; vals\[i] = fields\[k] &#x09;} &#x09;return strings.Join(vals, "\&") } // decodeFlatFields يقرأ كائن JSON مسطحاً ويحوّل كل قيمة إلى نص // (الأرقام تحتفظ بصياغتها الأصلية عبر json.Number). func decodeFlatFields(body string) (map\[string]string, error) { &#x09;var raw map\[string]interface{} &#x09;dec := json.NewDecoder(strings.NewReader(body)) &#x09;dec.UseNumber() &#x09;if err := dec.Decode(\&raw); err != nil { &#x09; return nil, fmt.Errorf("%w: %v", ErrMalformedBody, err) &#x09;} &#x09;out := make(map\[string]string, len(raw)) &#x09;for k, v := range raw { &#x09; switch x := v.(type) { &#x09; case string: &#x09; out\[k] = x &#x09; case json.Number: &#x09; out\[k] = x.String() &#x09; case bool: &#x09; out\[k] = strconv.FormatBool(x) &#x09; default: &#x09; return nil, fmt.Errorf("%w: الحقل %q ليس قيمة بسيطة", ErrMalformedBody, k) &#x09; } &#x09;} &#x09;return out, nil } func ParsePayLine(body string, secret string) (PaymentEvent, error) { &#x09;f, err := decodeFlatFields(body) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w", err) &#x09;} &#x09;// التحقق من التوقيع أولاً، قبل الوثوق بأي حقل. &#x09;if err := verifyHMAC(payLineSigningString(f), secret, f\["sign"]); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w", err) &#x09;} &#x09;if f\["txn\_id"] == "" { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w: txn\_id", ErrMissingField) &#x09;} &#x09;if f\["order\_no"] == "" { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w: order\_no", ErrMissingField) &#x09;} &#x09;status, err := normalizeStatus(f\["result"]) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w", err) &#x09;} &#x09;cents, err := parseDecimalCents(f\["amt"]) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w", err) &#x09;} &#x09;cur, err := normalizeCurrency(f\["cur"]) &#x09;if err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w", err) &#x09;} &#x09;sec, err := strconv.ParseInt(strings.TrimSpace(f\["ts"]), 10, 64) &#x09;if err != nil || sec <= 0 { &#x09; return PaymentEvent{}, fmt.Errorf("payline: %w: %q", ErrInvalidTime, f\["ts"]) &#x09;} &#x09;return PaymentEvent{ &#x09; Provider: ProviderPayLine, &#x09; EventID: f\["txn\_id"], &#x09; Status: status, &#x09; AmountCents: cents, &#x09; Currency: cur, &#x09; Reference: f\["order\_no"], &#x09; OccurredAt: time.Unix(sec, 0).UTC(), &#x09;}, nil } // --------------------------------------------------------------------------- // إزالة التكرار // --------------------------------------------------------------------------- // Dedupe يُبقي أول ظهور لكل (Provider:EventID) ويحافظ على الترتيب. func Dedupe(events \[]PaymentEvent) \[]PaymentEvent { &#x09;seen := make(map\[string]struct{}, len(events)) &#x09;out := make(\[]PaymentEvent, 0, len(events)) &#x09;for \_, e := range events { &#x09; key := e.Provider + ":" + e.EventID &#x09; if \_, dup := seen\[key]; dup { &#x09; continue &#x09; } &#x09; seen\[key] = struct{}{} &#x09; out = append(out, e) &#x09;} &#x09;return out } // --------------------------------------------------------------------------- // بيانات تجريبية (مفاتيح ثابتة، لا شيء عشوائي) // --------------------------------------------------------------------------- const ( &#x09;madaSecret = "demo-mada-pay-secret-key" &#x09;payLineSecret = "demo-payline-secret-key" ) func signMada(body, secret string) string { &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(body)) &#x09;return "sha256=" + hex.EncodeToString(mac.Sum(nil)) } // signedPayLineBody يبني جسم باي-لاين موقَّعاً؛ json.Marshal لخريطة يرتب المفاتيح فيكون الناتج حتمياً. func signedPayLineBody(fields map\[string]string, secret string) string { &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(payLineSigningString(fields))) &#x09;fields\["sign"] = hex.EncodeToString(mac.Sum(nil)) &#x09;b, \_ := json.Marshal(fields) &#x09;return string(b) } func printEvent(e PaymentEvent) { &#x09;fmt.Printf(" %-8s %-12s %-9s %8.2f %s ref=%s at=%s\\n", &#x09; e.Provider, e.EventID, e.Status, &#x09; float64(e.AmountCents)/100, e.Currency, e.Reference, &#x09; e.OccurredAt.Format(time.RFC3339)) } func main() { &#x09;const orderRef = "ORD-2026-1042" &#x09;// 1) مدى-بي: نجاح، 150.75 ر.س، بتوقيت +03:00 (سيُحوَّل إلى UTC). &#x09;madaBody := `{"event\_id":"mp\_evt\_9001","state":"SUCCESS","amount\_minor":15075,` + &#x09; `"currency":"SAR","merchant\_ref":"` + orderRef + `","created\_at":"2026-10-29T14:30:00+03:00"}` &#x09;madaSig := signMada(madaBody, madaSecret) &#x09;// 2) باي-لاين: نجاح لنفس الطلب، بأسماء وحالات وصيغ مختلفة (عملة بأحرف صغيرة، ts بالثواني). &#x09;payTS := strconv.FormatInt(time.Date(2026, 10, 29, 11, 31, 5, 0, time.UTC).Unix(), 10) &#x09;payFields := map\[string]string{ &#x09; "txn\_id": "pl\_7001", "result": "paid", "amt": "150.75", &#x09; "cur": "sar", "order\_no": orderRef, "ts": payTS, &#x09;} &#x09;payBody := signedPayLineBody(payFields, payLineSecret) &#x09;// 3) إعادة إرسال (retry) لنفس إشعار باي-لاين حرفياً. &#x09;payRetryBody := payBody &#x09;// 4) إشعار باي-لاين بتوقيع تالف: نغيّر المبلغ بعد التوقيع. &#x09;tamperedFields := map\[string]string{ &#x09; "txn\_id": "pl\_7002", "result": "completed", "amt": "999.00", &#x09; "cur": "SAR", "order\_no": "ORD-2026-1043", "ts": payTS, &#x09;} &#x09;tamperedBody := signedPayLineBody(tamperedFields, payLineSecret) &#x09;tamperedBody = strings.Replace(tamperedBody, `"999.00"`, `"1.00"`, 1) &#x09;type delivery struct { &#x09; name string &#x09; parse func() (PaymentEvent, error) &#x09;} &#x09;deliveries := \[]delivery{ &#x09; {"مدى-بي: نجاح", func() (PaymentEvent, error) { return ParseMadaPay(madaBody, madaSig, madaSecret) }}, &#x09; {"باي-لاين: نجاح", func() (PaymentEvent, error) { return ParsePayLine(payBody, payLineSecret) }}, &#x09; {"باي-لاين: إعادة إرسال (retry)", func() (PaymentEvent, error) { return ParsePayLine(payRetryBody, payLineSecret) }}, &#x09; {"باي-لاين: توقيع تالف", func() (PaymentEvent, error) { return ParsePayLine(tamperedBody, payLineSecret) }}, &#x09;} &#x09;fmt.Println("== معالجة الإشعارات الواردة ==") &#x09;var accepted \[]PaymentEvent &#x09;for i, d := range deliveries { &#x09; ev, err := d.parse() &#x09; if err != nil { &#x09; fmt.Printf("%d) %s -> مرفوض: %v (توقيع تالف؟ %t)\\n", i+1, d.name, err, errors.Is(err, ErrBadSignature)) &#x09; continue &#x09; } &#x09; fmt.Printf("%d) %s -> مقبول\\n", i+1, d.name) &#x09; accepted = append(accepted, ev) &#x09;} &#x09;unique := Dedupe(accepted) &#x09;fmt.Printf("\\n== الأحداث الموحدة بعد إزالة التكرار (%d من %d مقبولة) ==\\n", len(unique), len(accepted)) &#x09;for \_, e := range unique { &#x09; printEvent(e) &#x09;} &#x09;// عرض أخطاء التحويل المميزة على أمثلة صغيرة. &#x09;fmt.Println("\\n== فحوص الرفض (sentinel errors) ==") &#x09;checks := \[]struct { &#x09; name string &#x09; body string &#x09; want error &#x09;}{ &#x09; {"مبلغ صفر", `{"event\_id":"x1","state":"SUCCESS","amount\_minor":0,"currency":"SAR","merchant\_ref":"R","created\_at":"2026-10-29T10:00:00Z"}`, ErrInvalidAmount}, &#x09; {"مبلغ سالب", `{"event\_id":"x2","state":"SUCCESS","amount\_minor":-500,"currency":"SAR","merchant\_ref":"R","created\_at":"2026-10-29T10:00:00Z"}`, ErrInvalidAmount}, &#x09; {"عملة غير معروفة", `{"event\_id":"x3","state":"SUCCESS","amount\_minor":500,"currency":"XYZ","merchant\_ref":"R","created\_at":"2026-10-29T10:00:00Z"}`, ErrUnknownCurrency}, &#x09; {"وقت غير صالح", `{"event\_id":"x4","state":"SUCCESS","amount\_minor":500,"currency":"SAR","merchant\_ref":"R","created\_at":"29/10/2026"}`, ErrInvalidTime}, &#x09;} &#x09;for \_, c := range checks { &#x09; \_, err := ParseMadaPay(c.body, signMada(c.body, madaSecret), madaSecret) &#x09; fmt.Printf(" %-18s -> %t (%v)\\n", c.name, errors.Is(err, c.want), err) &#x09;} }
جChatGPT84.5 · 1,694 كلمة
سأبني الحزمة في ملف main.go باستخدام مكتبة Go القياسية فقط، مع توحيد الإشعارات والتحقق من التوقيع قبل قبول أي حدث، وإزالة التكرار وفق Provider + ":" + EventID مع الاحتفاظ بأول ظهور. هناك نقطة أمنية مهمة: الإشعار ذو التوقيع غير الصحيح يجب ألا يدخل قائمة الأحداث المقبولة أصلاً. كذلك، إزالة تكرار الإشعارات داخل الذاكرة لا تمنع تكرار التسوية بعد إعادة تشغيل البرنامج؛ في الإنتاج تحتاج إلى قيد فريد دائم على معرّف الحدث ضمن معاملة التسوية. ملف main.go يعتمد المثال التالي مخططاً واضحاً للحقول، مع قبول بعض الأسماء البديلة الشائعة بين البوابتين. توقيع PayLine يُحسب من أزواج key=value المرتبة أبجدياً، مع استثناء sign، ثم تُجمع باستخدام \&. وتُعامل قيمة amount\_cents بوصفها هللات/وحدات العملة الصغرى، وليس مبلغاً عشرياً. package main import ( &#x09;"crypto/hmac" &#x09;"crypto/sha256" &#x09;"encoding/hex" &#x09;"encoding/json" &#x09;"errors" &#x09;"fmt" &#x09;"sort" &#x09;"strconv" &#x09;"strings" &#x09;"time" ) var ( &#x09;ErrInvalidSignature = errors.New("invalid signature") &#x09;ErrInvalidAmount = errors.New("invalid amount") &#x09;ErrUnknownCurrency = errors.New("unknown currency") &#x09;ErrInvalidOccurredAt = errors.New("invalid OccurredAt") &#x09;ErrInvalidStatus = errors.New("invalid payment status") &#x09;ErrInvalidBody = errors.New("invalid JSON body") &#x09;ErrMissingField = errors.New("missing required field") ) type PaymentEvent struct { &#x09;Provider string &#x09;EventID string &#x09;Status string &#x09;AmountCents int64 &#x09;Currency string &#x09;Reference string &#x09;OccurredAt time.Time } // Fixed demo secrets. In production, keep secrets outside source code. const ( &#x09;madaSecret = "mada-demo-secret-2026" &#x09;paylineSecret = "payline-demo-secret-2026" ) var knownCurrencies = func() map\[string]bool { &#x09;// ISO 4217 alphabetic currency codes, including common fund/metal &#x09;// codes. This is a static validation list, not a live ISO registry. &#x09;codes := strings.Fields(` AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BOV BRL BSD BTN BWP BYN BZD CAD CDF CHE CHF CHW CLF CLP CNY COP COU CRC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HTG HUF IDR ILS INR IQD IRR ISK JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA MKD MMK MNT MOP MRU MUR MVR MWK MXN MXV MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SLL SOS SRD SSP STN SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD USN UYI UYU UYW UZS VED VES VND VUV WST XAF XAG XAU XBA XBB XBC XBD XCD XCG XDR XOF XPD XPF XPT XSU XTS XUA XXX YER ZAR ZMW ZWG `) &#x09;m := make(map\[string]bool, len(codes)) &#x09;for \_, c := range codes { &#x09; m\[c] = true &#x09;} &#x09;return m }() // JSON values are kept as RawMessage so numbers and strings can both // be accepted without losing their textual representation. type rawFields map\[string]json.RawMessage func decodeBody(body string) (rawFields, error) { &#x09;var fields rawFields &#x09;if err := json.Unmarshal(\[]byte(body), \&fields); err != nil || fields == nil { &#x09; return nil, ErrInvalidBody &#x09;} &#x09;return fields, nil } func fieldString(fields rawFields, names ...string) (string, bool) { &#x09;for \_, name := range names { &#x09; raw, ok := fields\[name] &#x09; if !ok { &#x09; continue &#x09; } &#x09; var s string &#x09; if err := json.Unmarshal(raw, \&s); err == nil { &#x09; return s, true &#x09; } &#x09; // Permit JSON integer/decimal tokens as text. &#x09; var n json.Number &#x09; if err := json.Unmarshal(raw, \&n); err == nil { &#x09; return n.String(), true &#x09; } &#x09;} &#x09;return "", false } func requiredField(fields rawFields, names ...string) (string, error) { &#x09;s, ok := fieldString(fields, names...) &#x09;if !ok || strings.TrimSpace(s) == "" { &#x09; return "", fmt.Errorf("%w: %s", ErrMissingField, strings.Join(names, "/")) &#x09;} &#x09;return s, nil } func normalizeStatus(s string) (string, error) { &#x09;switch strings.ToLower(strings.TrimSpace(s)) { &#x09;case "success", "succeeded", "paid", "completed": &#x09; return "succeeded", nil &#x09;case "failed", "failure", "declined", "cancelled", "canceled": &#x09; return "failed", nil &#x09;default: &#x09; return "", fmt.Errorf("%w: %q", ErrInvalidStatus, s) &#x09;} } func parseAmount(s string) (int64, error) { &#x09;n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64) &#x09;if err != nil || n <= 0 { &#x09; return 0, fmt.Errorf("%w: %q", ErrInvalidAmount, s) &#x09;} &#x09;return n, nil } func parseCurrency(s string) (string, error) { &#x09;c := strings.ToUpper(strings.TrimSpace(s)) &#x09;if len(c) != 3 || !knownCurrencies\[c] { &#x09; return "", fmt.Errorf("%w: %q", ErrUnknownCurrency, s) &#x09;} &#x09;return c, nil } func parseOccurredAt(s string) (time.Time, error) { &#x09;// Require an explicit timezone: RFC3339 / RFC3339Nano. &#x09;t, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(s)) &#x09;if err != nil { &#x09; return time.Time{}, fmt.Errorf("%w: %q", ErrInvalidOccurredAt, s) &#x09;} &#x09;return t.UTC(), nil } func normalize(fields rawFields, provider string) (PaymentEvent, error) { &#x09;var e PaymentEvent &#x09;e.Provider = provider &#x09;var err error &#x09;if e.EventID, err = requiredField(fields, &#x09; "event\_id", "eventId", "eventid", "id", "transaction\_id"); err != nil { &#x09; return e, err &#x09;} &#x09;status, err := requiredField(fields, &#x09; "status", "payment\_status", "paymentStatus", "state") &#x09;if err != nil { &#x09; return e, err &#x09;} &#x09;if e.Status, err = normalizeStatus(status); err != nil { &#x09; return e, err &#x09;} &#x09;amount, err := requiredField(fields, &#x09; "amount\_cents", "amountCents", "amount") &#x09;if err != nil { &#x09; return e, err &#x09;} &#x09;if e.AmountCents, err = parseAmount(amount); err != nil { &#x09; return e, err &#x09;} &#x09;currency, err := requiredField(fields, "currency", "currency\_code", "currencyCode") &#x09;if err != nil { &#x09; return e, err &#x09;} &#x09;if e.Currency, err = parseCurrency(currency); err != nil { &#x09; return e, err &#x09;} &#x09;if e.Reference, err = requiredField(fields, &#x09; "reference", "order\_reference", "orderReference", "order\_ref", "merchant\_reference"); err != nil { &#x09; return e, err &#x09;} &#x09;occurred, err := requiredField(fields, &#x09; "occurred\_at", "occurredAt", "created\_at", "createdAt", "timestamp") &#x09;if err != nil { &#x09; return e, err &#x09;} &#x09;if e.OccurredAt, err = parseOccurredAt(occurred); err != nil { &#x09; return e, err &#x09;} &#x09;return e, nil } func verifyHMAC(message, signature, secret string) error { &#x09;signature = strings.TrimSpace(signature) &#x09;signature = strings.TrimPrefix(signature, "sha256=") &#x09;got, err := hex.DecodeString(signature) &#x09;if err != nil { &#x09; return ErrInvalidSignature &#x09;} &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;\_, \_ = mac.Write(\[]byte(message)) &#x09;if !hmac.Equal(got, mac.Sum(nil)) { &#x09; return ErrInvalidSignature &#x09;} &#x09;return nil } // ParseMadaPay validates the signature over the exact original body bytes. func ParseMadaPay(body string, signatureHex string, secret string) (PaymentEvent, error) { &#x09;if err := verifyHMAC(body, signatureHex, secret); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;fields, err := decodeBody(body) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return normalize(fields, "mada\_pay") } // PayLine canonicalization: sort field names lexicographically, // exclude "sign", join key=value pairs using "\&". func payLineCanonical(fields rawFields) (string, error) { &#x09;keys := make(\[]string, 0, len(fields)) &#x09;values := make(map\[string]string, len(fields)) &#x09;for key := range fields { &#x09; if key == "sign" { &#x09; continue &#x09; } &#x09; value, ok := fieldString(fields, key) &#x09; if !ok { &#x09; return "", fmt.Errorf("%w: unsupported PayLine value for %q", ErrInvalidBody, key) &#x09; } &#x09; keys = append(keys, key) &#x09; values\[key] = value &#x09;} &#x09;sort.Strings(keys) &#x09;pairs := make(\[]string, 0, len(keys)) &#x09;for \_, key := range keys { &#x09; pairs = append(pairs, key+"="+values\[key]) &#x09;} &#x09;return strings.Join(pairs, "\&"), nil } func ParsePayLine(body string, secret string) (PaymentEvent, error) { &#x09;fields, err := decodeBody(body) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;signature, err := requiredField(fields, "sign") &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;canonical, err := payLineCanonical(fields) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;if err := verifyHMAC(canonical, signature, secret); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return normalize(fields, "payline") } // Dedupe keeps the first event for each provider/event ID pair. func Dedupe(events \[]PaymentEvent) \[]PaymentEvent { &#x09;seen := make(map\[string]struct{}, len(events)) &#x09;result := make(\[]PaymentEvent, 0, len(events)) &#x09;for \_, event := range events { &#x09; key := event.Provider + ":" + event.EventID &#x09; if \_, exists := seen\[key]; exists { &#x09; continue &#x09; } &#x09; seen\[key] = struct{}{} &#x09; result = append(result, event) &#x09;} &#x09;return result } // Demo helpers generate correctly signed sample payloads. func signHMAC(message, secret string) string { &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;\_, \_ = mac.Write(\[]byte(message)) &#x09;return hex.EncodeToString(mac.Sum(nil)) } func signPayLineBody(body, secret string) (string, error) { &#x09;fields, err := decodeBody(body) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;canonical, err := payLineCanonical(fields) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;// Re-encode as JSON with a generated sign field. &#x09;fields\["sign"], \_ = json.Marshal(signHMAC(canonical, secret)) &#x09;encoded, err := json.Marshal(fields) &#x09;if err != nil { &#x09; return "", err &#x09;} &#x09;return string(encoded), nil } func main() { &#x09;accepted := make(\[]PaymentEvent, 0, 4) &#x09;// Both providers report the same order, but have distinct event IDs. &#x09;madaBody := `{"event\_id":"mada-evt-001","status":"SUCCESS","amount\_cents":12500,"currency":"SAR","reference":"ORDER-1001","occurred\_at":"2026-10-29T10:00:00+03:00"}` &#x09;madaSignature := signHMAC(madaBody, madaSecret) &#x09;madaEvent, err := ParseMadaPay(madaBody, madaSignature, madaSecret) &#x09;if err != nil { &#x09; fmt.Println("MadaPay rejected:", err) &#x09;} else { &#x09; accepted = append(accepted, madaEvent) &#x09;} &#x09;paylineUnsigned := `{"eventId":"payline-evt-900","payment\_status":"paid","amount\_cents":12500,"currency":"sar","order\_ref":"ORDER-1001","created\_at":"2026-10-29T07:00:00Z"}` &#x09;paylineBody, err := signPayLineBody(paylineUnsigned, paylineSecret) &#x09;if err != nil { &#x09; panic(err) &#x09;} &#x09;paylineEvent, err := ParsePayLine(paylineBody, paylineSecret) &#x09;if err != nil { &#x09; fmt.Println("PayLine rejected:", err) &#x09;} else { &#x09; accepted = append(accepted, paylineEvent) &#x09; // Simulate an identical retry from PayLine. &#x09; accepted = append(accepted, paylineEvent) &#x09;} &#x09;// Tampered body: signature was calculated for a different payload. &#x09;tamperedBody := strings.Replace(madaBody, "12500", "12501", 1) &#x09;\_, err = ParseMadaPay(tamperedBody, madaSignature, madaSecret) &#x09;if err != nil { &#x09; fmt.Println("Tampered notification rejected:", err) &#x09;} &#x09;finalEvents := Dedupe(accepted) &#x09;fmt.Printf("\\nFinal events (%d):\\n", len(finalEvents)) &#x09;for \_, event := range finalEvents { &#x09; fmt.Printf( &#x09; "Provider=%s EventID=%s Status=%s AmountCents=%d Currency=%s Reference=%s OccurredAt=%s\\n", &#x09; event.Provider, &#x09; event.EventID, &#x09; event.Status, &#x09; event.AmountCents, &#x09; event.Currency, &#x09; event.Reference, &#x09; event.OccurredAt.Format(time.RFC3339Nano), &#x09; ) &#x09;} } التشغيل والتحقق احفظ الملف باسم main.go ثم شغّله: go run main.go الناتج المتوقع يتضمن حدثين مقبولين فقط؛ إشعار نجاح من كل بوابة، مع رفض التوقيع التالف وإزالة إعادة إرسال PayLine المكررة: Tampered notification rejected: invalid signature Final events (2): Provider=mada\_pay EventID=mada-evt-001 Status=succeeded AmountCents=12500 Currency=SAR Reference=ORDER-1001 OccurredAt=2026-10-29T07:00:00Z Provider=payline EventID=payline-evt-900 Status=succeeded AmountCents=12500 Currency=SAR Reference=ORDER-1001 OccurredAt=2026-10-29T07:00:00Z ملاحظات مهمة قبل الإنتاج: \- مخطط الحقول وأسماء الحقول البديلة في المثال افتراضات عملية؛ يجب مطابقتها مع التوثيق الرسمي لكل بوابة. \- يجب التأكد من خوارزمية التوقيع الدقيقة لدى PayLine، خصوصاً قواعد الترميز والقيم الفارغة وترتيب الحقول؛ أي اختلاف في بناء النص الموقّع سيؤدي إلى رفض إشعارات صحيحة. \- قائمة العملات ثابتة ومضمّنة في الملف؛ ينبغي مراجعتها دورياً مقابل معيار ISO 4217. \- Dedupe تزيل التكرار من الشريحة الحالية فقط. لمنع التسوية المكررة فعلياً، استخدم قيداً فريداً دائماً على (Provider, EventID) داخل قاعدة البيانات، ونفّذ تسجيل الحدث والتسوية ضمن معاملة ذرية واحدة. لا تُرجع نجاحاً للإشعار إلا بعد حفظه أو تسجيله بصورة موثوقة.
أDeepSeek81.0 · 1,686 كلمة
الحقول المرتبة أبجدياً (باستثناء حقل sign نفسه)، وتتحقق. هيكل موحّد: go type PaymentEvent struct { &#x20; Provider string // "mada\_pay" | "payline" &#x20; EventID string // معرّف فريد عند كل مزود &#x20; Status string // "succeeded" | "failed" — يجب توحيد الحالات ("SUCCESS","paid","completed" → succeeded) &#x20; AmountCents int64 &#x20; Currency string // ISO 4217 upper-case &#x20; Reference string // رقم طلبنا عند المزود &#x20; OccurredAt time.Time // بصيغة UTC } Dedupe(events \[]PaymentEvent) \[]PaymentEvent — تعيد قائمة بدون تكرار: المفتاح هو Provider + ":" + EventID، وعند التكرار يُبقى أول ظهور فقط مع الحفاظ على الترتيب. أخطاء التحويل: مبالغ سالبة أو صفر تُرفض، عملة غير معروفة تُرفض، OccurredAt بصيغة غير صالحة تُرفض — كلها بأخطاء مميزة القيمة (sentinel errors). بيانات تجريبية مضمّنة في main تُظهر: إشعارا نجاح من المزودين لنفس الطلب (منصوعين يدوياً بمفاتيح ثابتة في الملف)، إشعار payline مكرر، وإشعار بتوقيع تالف — مع طباعة الأحداث الموحدة النهائية. حتمي بالكامل، لا time.Now، لا قراءة من ملفات أو الشبكة، مكتبة قياسية فقط (crypto/hmac, crypto/sha256, encoding/hex, encoding/json, sort, strings, time). Here's a complete, deterministic, single-file Go solution matching the spec: go package main import ( &#x09;"crypto/hmac" &#x09;"crypto/sha256" &#x09;"encoding/hex" &#x09;"encoding/json" &#x09;"errors" &#x09;"fmt" &#x09;"sort" &#x09;"strings" &#x09;"time" ) // ---------- Sentinel errors ---------- var ( &#x09;ErrSignatureMismatch = errors.New("signature mismatch") &#x09;ErrMissingSignature = errors.New("missing signature") &#x09;ErrBadAmount = errors.New("amount must be positive") &#x09;ErrUnknownCurrency = errors.New("unknown currency") &#x09;ErrBadTimestamp = errors.New("invalid occurred\_at timestamp") &#x09;ErrMalformedBody = errors.New("malformed body") &#x09;ErrMissingField = errors.New("missing required field") &#x09;ErrUnknownStatus = errors.New("unknown status") ) // ---------- Unified event ---------- type PaymentEvent struct { &#x09;Provider string &#x09;EventID string &#x09;Status string &#x09;AmountCents int64 &#x09;Currency string &#x09;Reference string &#x09;OccurredAt time.Time } var knownCurrencies = map\[string]struct{}{ &#x09;"SAR": {}, "USD": {}, "EUR": {}, "AED": {}, "GBP": {}, &#x09;"KWD": {}, "BHD": {}, "OMR": {}, "QAR": {}, "EGP": {}, "JOD": {}, } // ---------- Helpers ---------- func hmacSHA256Hex(secret, payload string) string { &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(payload)) &#x09;return hex.EncodeToString(mac.Sum(nil)) } func normalizeStatus(provider, s string) (string, error) { &#x09;u := strings.ToUpper(strings.TrimSpace(s)) &#x09;switch u { &#x09;case "SUCCESS", "SUCCEEDED", "PAID", "COMPLETED", "CAPTURED", "OK": &#x09; return "succeeded", nil &#x09;case "FAILED", "FAILURE", "DECLINED", "ERROR", "CANCELED", "CANCELLED", "REJECTED": &#x09; return "failed", nil &#x09;} &#x09;return "", fmt.Errorf("%w: %q (provider=%s)", ErrUnknownStatus, s, provider) } func normalizeCurrency(c string) (string, error) { &#x09;u := strings.ToUpper(strings.TrimSpace(c)) &#x09;if \_, ok := knownCurrencies\[u]; !ok { &#x09; return "", fmt.Errorf("%w: %q", ErrUnknownCurrency, c) &#x09;} &#x09;return u, nil } func validateAmount(a int64) (int64, error) { &#x09;if a <= 0 { &#x09; return 0, fmt.Errorf("%w: %d", ErrBadAmount, a) &#x09;} &#x09;return a, nil } func parseUTC(s string) (time.Time, error) { &#x09;// Accept RFC3339 and a couple of common layouts; always force UTC. &#x09;layouts := \[]string{ &#x09; time.RFC3339Nano, &#x09; time.RFC3339, &#x09; "2006-01-02T15:04:05", &#x09; "2006-01-02 15:04:05", &#x09;} &#x09;for \_, l := range layouts { &#x09; if t, err := time.Parse(l, s); err == nil { &#x09; return t.UTC(), nil &#x09; } &#x09;} &#x09;return time.Time{}, fmt.Errorf("%w: %q", ErrBadTimestamp, s) } // ---------- Mada-Pay ---------- // Body is JSON; signature in header as "sha256=<hex>", HMAC over the whole raw body. type madaPayBody struct { &#x09;ID string `json:"id"` &#x09;Status string `json:"status"` &#x09;Amount json.RawMessage `json:"amount"` // could be number or string &#x09;Currency string `json:"currency"` &#x09;OrderRef string `json:"order\_ref"` &#x09;CreatedAt string `json:"created\_at"` } func ParseMadaPay(body string, signatureHex string, secret string) (PaymentEvent, error) { &#x09;// Verify signature (header format: "sha256=<hex>" or bare hex). &#x09;sig := strings.TrimSpace(signatureHex) &#x09;if sig == "" { &#x09; return PaymentEvent{}, ErrMissingSignature &#x09;} &#x09;if strings.HasPrefix(strings.ToLower(sig), "sha256=") { &#x09; sig = sig\[len("sha256="):] &#x09;} &#x09;expected := hmacSHA256Hex(secret, body) &#x09;// constant-time compare on decoded bytes &#x09;gotB, err1 := hex.DecodeString(sig) &#x09;expB, err2 := hex.DecodeString(expected) &#x09;if err1 != nil || err2 != nil || !hmac.Equal(gotB, expB) { &#x09; return PaymentEvent{}, fmt.Errorf("%w (mada\_pay)", ErrSignatureMismatch) &#x09;} &#x09;var raw madaPayBody &#x09;if err := json.Unmarshal(\[]byte(body), \&raw); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("%w: %v", ErrMalformedBody, err) &#x09;} &#x09;if raw.ID == "" || raw.Status == "" || raw.Currency == "" || raw.OrderRef == "" || raw.CreatedAt == "" { &#x09; return PaymentEvent{}, fmt.Errorf("%w (mada\_pay)", ErrMissingField) &#x09;} &#x09;// Amount may be number or numeric string. &#x09;var amount int64 &#x09;if len(raw.Amount) > 0 \&\& raw.Amount\[0] == '"' { &#x09; var s string &#x09; if err := json.Unmarshal(raw.Amount, \&s); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("%w: amount", ErrMalformedBody) &#x09; } &#x09; if \_, err := fmt.Sscanf(s, "%d", \&amount); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("%w: amount", ErrMalformedBody) &#x09; } &#x09;} else { &#x09; if err := json.Unmarshal(raw.Amount, \&amount); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("%w: amount", ErrMalformedBody) &#x09; } &#x09;} &#x09;status, err := normalizeStatus("mada\_pay", raw.Status) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;cur, err := normalizeCurrency(raw.Currency) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;if \_, err := validateAmount(amount); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;ts, err := parseUTC(raw.CreatedAt) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return PaymentEvent{ &#x09; Provider: "mada\_pay", &#x09; EventID: raw.ID, &#x09; Status: status, &#x09; AmountCents: amount, &#x09; Currency: cur, &#x09; Reference: raw.OrderRef, &#x09; OccurredAt: ts, &#x09;}, nil } // ---------- PayLine ---------- // Body is a form-encoded string "k1=v1\&k2=v2...". Signature in field "sign". // HMAC-SHA256 over alphabetically-sorted fields (excluding "sign") joined by "\&". // Value is hex. func ParsePayLine(body string, secret string) (PaymentEvent, error) { &#x09;fields := map\[string]string{} &#x09;var order \[]string &#x09;for \_, part := range strings.Split(body, "\&") { &#x09; if part == "" { &#x09; continue &#x09; } &#x09; kv := strings.SplitN(part, "=", 2) &#x09; if len(kv) != 2 { &#x09; return PaymentEvent{}, fmt.Errorf("%w (payline)", ErrMalformedBody) &#x09; } &#x09; k := kv\[0] &#x09; v := kv\[1] &#x09; fields\[k] = v &#x09; order = append(order, k) &#x09;} &#x09;\_ = order &#x09;sig, ok := fields\["sign"] &#x09;if !ok || sig == "" { &#x09; return PaymentEvent{}, ErrMissingSignature &#x09;} &#x09;// Build canonical string. &#x09;keys := make(\[]string, 0, len(fields)) &#x09;for k := range fields { &#x09; if k == "sign" { &#x09; continue &#x09; } &#x09; keys = append(keys, k) &#x09;} &#x09;sort.Strings(keys) &#x09;var b strings.Builder &#x09;for i, k := range keys { &#x09; if i > 0 { &#x09; b.WriteByte('\&') &#x09; } &#x09; b.WriteString(k) &#x09; b.WriteByte('=') &#x09; b.WriteString(fields\[k]) &#x09;} &#x09;expected := hmacSHA256Hex(secret, b.String()) &#x09;gotB, err1 := hex.DecodeString(sig) &#x09;expB, err2 := hex.DecodeString(expected) &#x09;if err1 != nil || err2 != nil || !hmac.Equal(gotB, expB) { &#x09; return PaymentEvent{}, fmt.Errorf("%w (payline)", ErrSignatureMismatch) &#x09;} &#x09;// Required fields (PayLine naming). &#x09;eventID := fields\["transaction\_id"] &#x09;statusRaw := fields\["status"] &#x09;amountStr := fields\["amount"] &#x09;currencyRaw := fields\["currency"] &#x09;ref := fields\["order\_id"] &#x09;tsStr := fields\["created\_at"] &#x09;if eventID == "" || statusRaw == "" || amountStr == "" || currencyRaw == "" || ref == "" || tsStr == "" { &#x09; return PaymentEvent{}, fmt.Errorf("%w (payline)", ErrMissingField) &#x09;} &#x09;var amount int64 &#x09;if \_, err := fmt.Sscanf(amountStr, "%d", \&amount); err != nil { &#x09; return PaymentEvent{}, fmt.Errorf("%w: amount", ErrMalformedBody) &#x09;} &#x09;status, err := normalizeStatus("payline", statusRaw) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;cur, err := normalizeCurrency(currencyRaw) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;if \_, err := validateAmount(amount); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;ts, err := parseUTC(tsStr) &#x09;if err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return PaymentEvent{ &#x09; Provider: "payline", &#x09; EventID: eventID, &#x09; Status: status, &#x09; AmountCents: amount, &#x09; Currency: cur, &#x09; Reference: ref, &#x09; OccurredAt: ts, &#x09;}, nil } // ---------- Dedupe ---------- func Dedupe(events \[]PaymentEvent) \[]PaymentEvent { &#x09;seen := make(map\[string]struct{}, len(events)) &#x09;out := make(\[]PaymentEvent, 0, len(events)) &#x09;for \_, e := range events { &#x09; k := e.Provider + ":" + e.EventID &#x09; if \_, ok := seen\[k]; ok { &#x09; continue &#x09; } &#x09; seen\[k] = struct{}{} &#x09; out = append(out, e) &#x09;} &#x09;return out } // ---------- Demo ---------- const ( &#x09;madaSecret = "mada\_secret\_demo\_key\_123" &#x09;paylineSecret = "payline\_secret\_demo\_key\_456" ) func buildMadaBody() string { &#x09;// Hand-crafted JSON. Amount is number (cents). &#x09;return `{"id":"MP-EVT-9001","status":"SUCCESS","amount":15000,"currency":"sar","order\_ref":"ORDER-42","created\_at":"2024-03-01T12:34:56Z"}` } func buildPayLineBody(signHex string) string { &#x09;// Fields (order doesn't matter for signing; signing canonicalizes). &#x09;// Status uses "completed" to exercise mapping. &#x09;return "transaction\_id=PL-TX-7001" + &#x09; "\&status=completed" + &#x09; "\&amount=15000" + &#x09; "\&currency=SAR" + &#x09; "\&order\_id=ORDER-42" + &#x09; "\&created\_at=2024-03-01T12:35:10Z" + &#x09; "\&sign=" + signHex } func signPayLine(fields map\[string]string, secret string) string { &#x09;keys := make(\[]string, 0, len(fields)) &#x09;for k := range fields { &#x09; keys = append(keys, k) &#x09;} &#x09;sort.Strings(keys) &#x09;var b strings.Builder &#x09;for i, k := range keys { &#x09; if i > 0 { &#x09; b.WriteByte('\&') &#x09; } &#x09; b.WriteString(k) &#x09; b.WriteByte('=') &#x09; b.WriteString(fields\[k]) &#x09;} &#x09;return hmacSHA256Hex(secret, b.String()) } func main() { &#x09;// --- Mada-Pay success --- &#x09;madaBody := buildMadaBody() &#x09;madaSig := "sha256=" + hmacSHA256Hex(madaSecret, madaBody) &#x09;madaEvt, err := ParseMadaPay(madaBody, madaSig, madaSecret) &#x09;if err != nil { &#x09; fmt.Println("mada parse error:", err) &#x09;} &#x09;// --- PayLine success (same order, different provider) --- &#x09;plFields := map\[string]string{ &#x09; "transaction\_id": "PL-TX-7001", &#x09; "status": "completed", &#x09; "amount": "15000", &#x09; "currency": "SAR", &#x09; "order\_id": "ORDER-42", &#x09; "created\_at": "2024-03-01T12:35:10Z", &#x09;} &#x09;plBody := buildPayLineBody(signPayLine(plFields, paylineSecret)) &#x09;plEvt, err := ParsePayLine(plBody, paylineSecret) &#x09;if err != nil { &#x09; fmt.Println("payline parse error:", err) &#x09;} &#x09;// --- PayLine duplicate (retry) --- &#x09;plDupBody := buildPayLineBody(signPayLine(plFields, paylineSecret)) &#x09;plDupEvt, err := ParsePayLine(plDupBody, paylineSecret) &#x09;if err != nil { &#x09; fmt.Println("payline dup parse error:", err) &#x09;} &#x09;// --- Tampered signature --- &#x09;tamperedSig := "sha256=" + strings.Repeat("a", 64) &#x09;\_, err = ParseMadaPay(madaBody, tamperedSig, madaSecret) &#x09;fmt.Println("tampered mada -> error:", err) &#x09;if !errors.Is(err, ErrSignatureMismatch) { &#x09; fmt.Println("WARNING: expected ErrSignatureMismatch") &#x09;} &#x09;// --- Dedupe \& print --- &#x09;all := \[]PaymentEvent{madaEvt, plEvt, plDupEvt} &#x09;final := Dedupe(all) &#x09;fmt.Println("\\nUnified events (after dedupe):") &#x09;for \_, e := range final { &#x09; fmt.Printf(" provider=%s event\_id=%s status=%s amount=%d %s ref=%s at=%s\\n", &#x09; e.Provider, e.EventID, e.Status, e.AmountCents, e.Currency, &#x09; e.Reference, e.OccurredAt.Format(time.RFC3339)) &#x09;} &#x09;fmt.Printf("\\nTotal parsed=%d, after dedupe=%d\\n", len(all), len(final)) } Key design decisions Signature verification is constant-time via hmac.Equal on decoded bytes; any mismatch (including malformed hex) maps to ErrSignatureMismatch. Mada-Pay accepts the header as sha256=<hex> or bare <hex>, and HMACs the entire raw body (no re-marshaling), matching the spec. PayLine canonicalizes by sorting all non-sign keys alphabetically, joins with k=v\&..., then HMACs. Signature stored in sign field. Field unification: id/transaction\_id → EventID; status/status mapped through normalizeStatus; order\_ref/order\_id → Reference; created\_at parsed with several layouts then forced to UTC. Sentinels are all distinct so callers can branch via errors.Is. Validation is ordered so signature errors surface before field/amount errors. Dedupe preserves first occurrence and order using a map\[string]struct{} keyed on Provider:EventID. Deterministic demo: no time.Now, no network, no files; secrets and payloads are constants. The tampered-signature case is asserted against ErrSignatureMismatch via errors.Is.
دGemini67.5 · 1,069 كلمة
إليك حزمة متكاملة في ملف واحد main.go باللغة Go، تستوفي جميع الشروط والمتطلبات المذكورة بدون أي اعتماديات خارجية أو اتصال شبكي، وتتضمن الدوال المطلوبة، المعالجة الآمنة للتوقيعات (HMAC-SHA256)، الأخطاء المعيارية (Sentinel Errors)، دالة إزالة التكرار (Dedupe)، وبيانات تجريبية شاملة في الدالة main. ملف التنفيذ: main.go Go package main import ( &#x09;"crypto/hmac" &#x09;"crypto/sha256" &#x09;"encoding/hex" &#x09;"encoding/json" &#x09;"errors" &#x09;"fmt" &#x09;"sort" &#x09;"strings" &#x09;"time" ) // الأخطاء المعيارية (Sentinel Errors) المحددة للمتطلبات var ( &#x09;ErrInvalidAmount = errors.New("invalid amount: must be greater than zero") &#x09;ErrInvalidCurrency = errors.New("invalid or unknown currency") &#x09;ErrInvalidTimestamp = errors.New("invalid timestamp format") &#x09;ErrInvalidSignature = errors.New("invalid signature") ) // PaymentEvent الهيكل الموحد لأحداث الدفع type PaymentEvent struct { &#x09;Provider string // "mada\_pay" | "payline" &#x09;EventID string // معرّف فريد عند كل مزود &#x09;Status string // "succeeded" | "failed" &#x09;AmountCents int64 // المبلغ بالهللات/السنتات &#x09;Currency string // ISO 4217 upper-case &#x09;Reference string // رقم طلبنا عند المزود &#x09;OccurredAt time.Time // وقت الحدوث بصيغة UTC } // --------------------------------------------------------- // 1. ParseMadaPay // --------------------------------------------------------- func ParseMadaPay(body string, signatureHex string, secret string) (PaymentEvent, error) { &#x09;// التحقق من صيغة التوقيع sha256=<hex> &#x09;const prefix = "sha256=" &#x09;if !strings.HasPrefix(signatureHex, prefix) { &#x09; return PaymentEvent{}, ErrInvalidSignature &#x09;} &#x09;receivedHex := strings.TrimPrefix(signatureHex, prefix) &#x09;// التحقق من صحة توقيع HMAC-SHA256 للجسم كاملاً &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(body)) &#x09;expectedHex := hex.EncodeToString(mac.Sum(nil)) &#x09;if !hmac.Equal(\[]byte(receivedHex), \[]byte(expectedHex)) { &#x09; return PaymentEvent{}, ErrInvalidSignature &#x09;} &#x09;// هيكل بيانات Mada-Pay الداخلي &#x09;var raw struct { &#x09; EventID string `json:"event\_id"` &#x09; Status string `json:"status"` &#x09; Amount int64 `json:"amount\_cents"` &#x09; Currency string `json:"currency"` &#x09; Reference string `json:"reference"` &#x09; OccurredAt string `json:"occurred\_at"` &#x09;} &#x09;if err := json.Unmarshal(\[]byte(body), \&raw); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;return validateAndNormalize( &#x09; "mada\_pay", &#x09; raw.EventID, &#x09; raw.Status, &#x09; raw.Amount, &#x09; raw.Currency, &#x09; raw.OccurredAt, &#x09; raw.Reference, &#x09;) } // --------------------------------------------------------- // 2. ParsePayLine // --------------------------------------------------------- func ParsePayLine(body string, secret string) (PaymentEvent, error) { &#x09;var data map\[string]any &#x09;if err := json.Unmarshal(\[]byte(body), \&data); err != nil { &#x09; return PaymentEvent{}, err &#x09;} &#x09;receivedSign, ok := data\["sign"].(string) &#x09;if !ok { &#x09; return PaymentEvent{}, ErrInvalidSignature &#x09;} &#x09;// استبعاد حقل sign لحساب التوقيع &#x09;delete(data, "sign") &#x09;// ترتيب الحقول أبجدياً حسب المفتاح &#x09;var keys \[]string &#x09;for k := range data { &#x09; keys = append(keys, k) &#x09;} &#x09;sort.Strings(keys) &#x09;// تجميع القيم المرتبة بفاصل \& &#x09;var values \[]string &#x09;for \_, k := range keys { &#x09; values = append(values, valueToString(data\[k])) &#x09;} &#x09;stringToSign := strings.Join(values, "\&") &#x09;// حساب التوقيع المتوقع &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(stringToSign)) &#x09;expectedSign := hex.EncodeToString(mac.Sum(nil)) &#x09;if !hmac.Equal(\[]byte(receivedSign), \[]byte(expectedSign)) { &#x09; return PaymentEvent{}, ErrInvalidSignature &#x09;} &#x09;// استخراج البيانات بنجاح بعد التحقق &#x09;var raw struct { &#x09; TxID string `json:"tx\_id"` &#x09; State string `json:"state"` &#x09; Amount int64 `json:"amount"` &#x09; Curr string `json:"curr"` &#x09; OrderRef string `json:"order\_ref"` &#x09; Time string `json:"time"` &#x09;} &#x09;\_ = json.Unmarshal(\[]byte(body), \&raw) &#x09;return validateAndNormalize( &#x09; "payline", &#x09; raw.TxID, &#x09; raw.State, &#x09; raw.Amount, &#x09; raw.Curr, &#x09; raw.Time, &#x09; raw.OrderRef, &#x09;) } // --------------------------------------------------------- // 3. Dedupe // --------------------------------------------------------- func Dedupe(events \[]PaymentEvent) \[]PaymentEvent { &#x09;seen := make(map\[string]bool) &#x09;var result \[]PaymentEvent &#x09;for \_, e := range events { &#x09; key := fmt.Sprintf("%s:%s", e.Provider, e.EventID) &#x09; if !seen\[key] { &#x09; seen\[key] = true &#x09; result = append(result, e) &#x09; } &#x09;} &#x09;return result } // --------------------------------------------------------- // دوال مساعدة للتحقق، التطبيع، وتحويل القيم // --------------------------------------------------------- func validateAndNormalize(provider, eventID, rawStatus string, amount int64, currency string, rawTime string, reference string) (PaymentEvent, error) { &#x09;// 1. التحقق من المبلغ (يجب أن يكون أكبر من صفر) &#x09;if amount <= 0 { &#x09; return PaymentEvent{}, ErrInvalidAmount &#x09;} &#x09;// 2. التحقق من العملة (ISO 4217 upper-case مكونة من 3 حروف) &#x09;curr := strings.ToUpper(strings.TrimSpace(currency)) &#x09;if len(curr) != 3 { &#x09; return PaymentEvent{}, ErrInvalidCurrency &#x09;} &#x09;// 3. التحقق من وقت الحدوث (OccurredAt) &#x09;t, err := time.Parse(time.RFC3339, rawTime) &#x09;if err != nil { &#x09; return PaymentEvent{}, ErrInvalidTimestamp &#x09;} &#x09;// 4. توحيد الحالات (Status) &#x09;statusLower := strings.ToLower(strings.TrimSpace(rawStatus)) &#x09;var normalizedStatus string &#x09;switch statusLower { &#x09;case "success", "paid", "completed", "succeeded": &#x09; normalizedStatus = "succeeded" &#x09;case "failed", "failure", "declined": &#x09; normalizedStatus = "failed" &#x09;default: &#x09; normalizedStatus = "failed" &#x09;} &#x09;return PaymentEvent{ &#x09; Provider: provider, &#x09; EventID: eventID, &#x09; Status: normalizedStatus, &#x09; AmountCents: amount, &#x09; Currency: curr, &#x09; Reference: reference, &#x09; OccurredAt: t.UTC(), &#x09;}, nil } func valueToString(v any) string { &#x09;switch val := v.(type) { &#x09;case string: &#x09; return val &#x09;case float64: &#x09; if val == float64(int64(val)) { &#x09; return fmt.Sprintf("%d", int64(val)) &#x09; } &#x09; return fmt.Sprintf("%f", val) &#x09;case bool: &#x09; if val { &#x09; return "true" &#x09; } &#x09; return "false" &#x09;default: &#x09; return fmt.Sprintf("%v", val) &#x09;} } // مساعدة لتوليد توقيع Mada-Pay تجريبياً func generateMadaSignature(body, secret string) string { &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(body)) &#x09;return "sha256=" + hex.EncodeToString(mac.Sum(nil)) } // مساعدة لتوليد توقيع Pay-Line تجريبياً func generatePayLineSignature(body, secret string) string { &#x09;var data map\[string]any &#x09;\_ = json.Unmarshal(\[]byte(body), \&data) &#x09;delete(data, "sign") &#x09;var keys \[]string &#x09;for k := range data { &#x09; keys = append(keys, k) &#x09;} &#x09;sort.Strings(keys) &#x09;var values \[]string &#x09;for \_, k := range keys { &#x09; values = append(values, valueToString(data\[k])) &#x09;} &#x09;stringToSign := strings.Join(values, "\&") &#x09;mac := hmac.New(sha256.New, \[]byte(secret)) &#x09;mac.Write(\[]byte(stringToSign)) &#x09;return hex.EncodeToString(mac.Sum(nil)) } // --------------------------------------------------------- // الدالة الرئيسية للتجربة (Main) // --------------------------------------------------------- func main() { &#x09;madaSecret := "secret\_mada\_123" &#x09;paylineSecret := "secret\_payline\_456" &#x09;// 1. إشعار نجاح من Mada-Pay &#x09;madaBody := `{"event\_id":"mp\_evt\_1001","status":"SUCCESS","amount\_cents":15000,"currency":"SAR","reference":"ORD-999","occurred\_at":"2026-10-29T10:00:00Z"}` &#x09;madaSig := generateMadaSignature(madaBody, madaSecret) &#x09;event1, err1 := ParseMadaPay(madaBody, madaSig, madaSecret) &#x09;if err1 != nil { &#x09; fmt.Println("Error parsing MadaPay:", err1) &#x09;} &#x09;// 2. إشعار نجاح من Pay-Line (مع توليد التوقيع وحقنه في الجسم) &#x09;paylineBodyTemplate := `{"amount":15000,"curr":"SAR","order\_ref":"ORD-999","sign":"%s","time":"2026-10-29T10:01:00Z","state":"completed","tx\_id":"pl\_txn\_5001"}` &#x09;// لتوليد التوقيع، نحتاج أولاً وضع علامة مؤقتة أو بناء الخريطة وترتيبها &#x09;// سنقوم بحساب التوقيع الحقيقي للقيم المرتبة أبجديامع المفاتيح: amount, curr, order\_ref, state, time, tx\_id &#x09;plTempBody := `{"amount":15000,"curr":"SAR","order\_ref":"ORD-999","sign":"","time":"2026-10-29T10:01:00Z","state":"completed","tx\_id":"pl\_txn\_5001"}` &#x09;plSig := generatePayLineSignature(plTempBody, paylineSecret) &#x09;paylineBody := fmt.Sprintf(paylineBodyTemplate, plSig) &#x09;event2, err2 := ParsePayLine(paylineBody, paylineSecret) &#x09;if err2 != nil { &#x09; fmt.Println("Error parsing PayLine:", err2) &#x09;} &#x09;// 3. إشعار Pay-Line مكرر (نفس الـ EventID: pl\_txn\_5001) &#x09;event3, err3 := ParsePayLine(paylineBody, paylineSecret) &#x09;if err3 != nil { &#x09; fmt.Println("Error parsing duplicate PayLine:", err3) &#x09;} &#x09;// 4. إشعار بتوقيع تالف (سيفشل التحقق) &#x09;\_, err4 := ParseMadaPay(madaBody, "sha256=badsignature123", madaSecret) &#x09;fmt.Printf("Test Corrupted Signature (Expected Error): %v\\n\\n", err4) &#x09;// 5. إشعار بمبلغ غير صالح (صفر أو سالب - سيفشل التحقق) &#x09;badAmountBody := `{"event\_id":"mp\_evt\_1002","status":"SUCCESS","amount\_cents":0,"currency":"SAR","reference":"ORD-100","occurred\_at":"2026-10-29T10:05:00Z"}` &#x09;badAmountSig := generateMadaSignature(badAmountBody, madaSecret) &#x09;\_, err5 := ParseMadaPay(badAmountBody, badAmountSig, madaSecret) &#x09;fmt.Printf("Test Invalid Amount (Expected Error): %v\\n\\n", err5) &#x09;// تجميع الأحداث (بما فيها التكرار) وتطبيق دالة Dedupe &#x09;rawEvents := \[]PaymentEvent{event1, event2, event3} &#x09;dedupedEvents := Dedupe(rawEvents) &#x09;// طباعة النتائج النهائية الموحدة &#x09;fmt.Println("=== Final Deduped Payment Events ===") &#x09;for i, ev := range dedupedEvents { &#x09; fmt.Printf("\[%d] Provider: %s | EventID: %s | Status: %s | Amount: %d %s | Ref: %s | Time: %s\\n", &#x09; i+1, ev.Provider, ev.EventID, ev.Status, ev.AmountCents, ev.Currency, ev.Reference, ev.OccurredAt.Format(time.RFC3339)) &#x09;} }