حزمة SDK لـ .NET
Anis.Partners — لـ .NET 8 و .NET 10.
- الحزمة: nuget.org/packages/Anis.Partners
- الشيفرة المصدرية: github.com/anis-ly/anis.partners-sdk-dotnet
أداة مساعدة اختيارية، تُقدَّم كما هي
كل حزم SDK من أنيس مجانية وتُقدَّم بموجب رخصة MIT «كما هي»، دون أي ضمان من أي نوع. ولا تتحمّل أنيس (Aniscom for Technical Services) أي مسؤولية أو التزام عن استخدامها أو عن أي خسارة تنشأ عنه. وتبقى أنت المسؤول عن ربط نظامك — تسجيل طلبات الشراء قبل إرسالها، والاستئناف، وحفظ المفاتيح، والاختبار. الشيفرة المصدرية علنية: اقرأها لتفهم بدقة ما تفعله قبل أن تعتمد عليها. ولا تحتاج إلى حزمة SDK أصلًا — يمكنك استدعاء واجهة API لشركاء أنيس مباشرة باتباع دليل الاستخدام وصفحة مرجع API.
حزمة SDK بالإصدار 2.2.0. إصدارات 1.x مهجورة؛ راجع سجل التغييرات.
dotnet add package Anis.Partners --version 2.2.0
سجّلها
builder.Services
.AddAnisPartners(builder.Configuration.GetSection("AnisPartners"))
.WithSigner(EcdsaP256Signer.FromPemFile("/secure/partner-key.pem", keyId));
{
"AnisPartners": {
"Authority": "https://<the address Anis gave you>",
"SignatureLifetime": "00:01:00",
"AcceptLanguage": "English",
"SigningKeyCacheDuration": "00:05:00",
"Timeout": "00:00:30"
}
}
ثم خذ IAnisPartnersClient من الحاوية. تُفحص الإعدادات عند بدء تشغيل تطبيقك: فغياب العنوان، أو مدة صلاحية توقيع
خارج النطاق من 1 إلى 60 ثانية، أو مدة حفظ للمفاتيح تتجاوز 5 دقائق، يوقفه عند تلك النقطة.
المُوقِّع خطوة منفصلة عن قصد — إذ لا يوجد خيار افتراضي آمن لمكان حفظ مفتاحك الخاص. يوقّع EcdsaP256Signer
باستخدام ملف مفتاح؛ أما مع خزنة أسرار أو وحدة أمان عتادية، فنفّذ IRequestSigner — معرّف مفتاحك، ودالة توقّع
البايتات وتعيد التوقيع ذا الـ 64 بايت — ولا يدخل المفتاح إلى برنامجك أبدًا.
استخدمها
var profile = await anis.Profile.GetAsync(ct);
await foreach (var wallet in anis.Wallets.ListAsync(ct)) // pages followed for you
Console.WriteLine($"{wallet.Name} {wallet.Balance}");
await foreach (var card in anis.Catalogue.ListCardsAsync(walletId, subcategoryId, ct)) // every page, followed for you
Console.WriteLine($"{card.Name?.En} {card.UnitPrice}");
var credential = await anis.OwnedCards.RevealAsync(walletId, soldCardId, ct);
ما الذي تتحقق منه
منذ الإصدار 2.0.1، تتحقق الحزمة من كل ردّ على الاستدعاءات التي توقّع أنيس ردودها — تقديم طلب شراء، وقراءة
طلب شراء، وكشف بطاقة، وكشف بطاقات فاتورة، واستدعاءات تسجيل المفتاح الأربعة، وتعليق التطبيق، وإلغاء المفتاح، والفحص الذاتي للتوقيع — نجاحًا كان الردّ
أم رفضًا. وتشترط وجود التوقيع هناك: فالردّ على أحد هذه الاستدعاءات الذي لا يحمل توقيعًا صحيحًا يُتخلَّص منه
(UnverifiableResponseException)، ويعود في طلب الشراء في صورة OrderOutcomeUnknown. ولا تتحقق من استدعاءات المعلومات — ملفك التعريفي
والمحافظ والكتالوج وبطاقاتك المملوكة (القائمة والقراءة): فردودها لا تحمل توقيعًا، وحماية TLS تكفيها. وما زالت توقّع كل
استدعاء.
راجع التحقق من الردود لمعرفة أي الردود موقَّعة.
طلبات الشراء
var operationId = Guid.NewGuid(); // yours: store it with the request BEFORE sending
var outcome = await anis.Orders.CreateAsync(walletId, operationId, new CreateOrderRequest
{
CardId = card.Id,
Quantity = 2,
ExpectedUnitPrice = card.UnitPrice!.Value,
ExpectedTotal = card.UnitPrice!.Value.Multiply(2), // exact decimal
}, ct);
await (outcome switch
{
OrderCompleted c when c.CodesWithheld => ReportCodesWithheldAsync(operationId, ct), // paid, codes withheld: do not buy again; write to support@anis.ly
OrderCompleted c => vault.StoreAsync(c.Credentials, ct), // store FIRST
OrderReplayed r => EnsureStoredAsync(r.Order, ct), // sent earlier: if you never stored them, reveal by r.Order.InvoiceId
OrderProcessing p => ResumeAfterAsync(operationId, p.RetryAfter), // may still complete
OrderOutcomeUnknown u => ResumeAfterAsync(operationId, u.SuggestedDelay), // timed out, connection lost, an untrusted answer, a rate limit, or an access refusal
OrderNotPlaced n => CloseAsNotPlacedAsync(operationId, n.Refusal), // nothing bought: fix the cause, new order, new id
_ => throw new InvalidOperationException($"Unhandled order outcome {outcome}"),
});
يرسل ResumeAsync طلب الشراء المحفوظ مرة أخرى بالمعرّف نفسه، ويعيد النتائج الخمس نفسها.
يعيد CreateAsync المرفوض بسبب حدّ معدّل الاستدعاءات OrderOutcomeUnknown، وتكون قيمة Retry-After
الموقَّعة هي SuggestedDelay. وتعيد حالات رفض الوصول (invalid_credentials وinsufficient_scope
وwallet_not_granted) أيضًا OrderOutcomeUnknown، بمهلة مقترحة 60 ثانية: أعِد الوصول ثم استأنف بالمعرّف نفسه.
تعود حالات الرفض وانتهاء المهلة والردود التي يتعذّر التحقق منها في صورة نتائج. أما الطلبات غير الصالحة، بما فيها
سعر الوحدة الذي لا يزيد على صفر (ArgumentOutOfRangeException قبل الإرسال)، وإخفاق المُوقِّع والإلغاء
الصادر منك، فقد ترمي استثناءً. راجع طلبات الشراء والاستعادة للقواعد كاملةً.
تكون OrderCompleted.CodesWithheld مساوية لـ true حين يكون طلب الشراء مدفوعًا ومكتملًا لكن رموزه محجوبة: لا
تشترِه مرة أخرى. كشف البطاقات المحجوبة رموزها يُرفض، أما بقية بطاقات الطلب فيمكن كشفها. اكتب إلى
support@anis.ly واذكر معرّف العملية.
منذ الإصدار 2.0.1 تستخدم الحزمة CardSummary داخل البطاقة المملوكة والكشف وsoldCards في طلب الشراء. تحمل البطاقة
المملوكة SoldCardId وOperationId؛ أما القيمة المطبوعة والشعار والفئة الفرعية والفئة فداخل Card.
وللبطاقة المكشوفة البنية الكاملة نفسها داخل طلب الشراء. عملة المحفظة هي Balance.Currency.
حالات الرفض
لا يرمي طلب الشراء الرفضَ استثناءً: يعود بدلًا من ذلك في صورة OrderNotPlaced (في Refusal)، أو في صورة
OrderOutcomeUnknown (في Cause) حين يُحتمل أن يكتمل الطلب رغم ذلك أو أن تكون محاولة سابقة به قد تمّت. أما بقية استدعاءات الحزمة فترمي الرفض استثناءً.
والرفض هو AnisApiException (أو صنف فرعي منه للحالات التي تعالجها بطريقة مختلفة) يحمل Code وRequestId
وRetryAfter وIsReplayed. وكل صفحة خطأ تذكر نوعه.
ثلاثة استثناءات أخرى تعني شيئًا مختلفًا:
| الاستثناء | المعنى | في طلب الشراء |
|---|---|---|
UnverifiableResponseException |
ردّ على استدعاء توقّع أنيس ردوده تعذّر إثبات أنه صادر من أنيس؛ فجرى التخلّص منه | لا يُرمى — يعود الطلب في صورة OrderOutcomeUnknown: استأنف بالمعرّف نفسه |
LimitExceededException (spending_limit_exceeded، 429) |
كان الطلب سيتجاوز سقف إنفاق التطبيق أو المحفظة في النافذة الزمنية الحالية؛ لم يُشترَ شيء ولم يُخصم شيء | لا يُرمى — OrderNotPlaced. بعد RetryAfter قدّم طلبًا جديدًا بمعرّف جديد؛ ولا تستأنف هذا الطلب |
RequestSigningException |
أخفق المُوقِّع لديك؛ لم يُرسَل شيء | يُرمى — أرسله مرة أخرى بعد أن يعمل المُوقِّع |
TaskCanceledException، HttpRequestException |
انتهت المهلة، أو فشل الاتصال | لا يُرمى — OrderOutcomeUnknown: استأنف بالمعرّف نفسه. وحده الإلغاء الصادر منك يُرمى |
عدة تطبيقات أنيس في مضيف واحد
builder.Services.AddAnisPartners("brand-a", config.GetSection("AnisPartners:BrandA")).WithSigner(signerA);
builder.Services.AddAnisPartners("brand-b", config.GetSection("AnisPartners:BrandB")).WithSigner(signerB);
var brandA = factory.GetClient("brand-a"); // IAnisPartnersClientFactory, or [FromKeyedServices("brand-a")]
لكل تطبيق خط معالجة ومفتاح خاصان به، وتحمل تتبّعاته ومقاييسه anis.client.
لا إعادة محاولات تلقائية على هذا العميل
سياسة إعادة المحاولة التي تُضاف إلى كل عميل HTTP (مثل الإعدادات الافتراضية لخدمات .NET Aspire) تتخلّى عن كل
محاولة بعد 10 ثوانٍ افتراضيًا، أي قبل الـ 30 ثانية التي تعتمدها حزمة SDK. عندها قد يكتمل طلب شراء بطيء
لدى أنيس بينما يُتخلَّص من ردّه — ومعه رموز البطاقات. أعد محاولة عمليات القراءة بنفسك إن شئت؛ أما طلبات الشراء
فاستعِدها بـ ResumeAsync.
المراقبة
التتبّعات والمقاييس تحت Anis.Partners.Sdk؛ والسجلات عبر نظام التسجيل الخاص بك. راجع
المراقبة لمعرفة أسماء المقاييس وما يستحق التنبيه عليه.
إذا كان مضيفك يستبدل الحاوية المدمجة في .NET
تستخدم حزمة SDK الخدمات ذات المفاتيح (keyed services) في .NET 8. ويجب أن تدعمها أي حاوية من طرف ثالث —
وكلٌّ من Autofac 9 أو أحدث، وLamar 12.1 أو أحدث، وSimpleInjector يدعمها.
وإلا فأنشئ العميل من دون الحاوية: يمنحك AnisPartnersClient.Create(options, signer) العميلَ نفسه بالتوقيع والتحقق. أنشئه مرة واحدة واحتفظ به.
الدوال وردود البطاقات
تستخدم استدعاءات البطاقات المملوكة /v1/wallets/{walletId}/owned-cards، وقراءة /{soldCardId}
وكشف /{soldCardId}/reveal، و/v1/wallets/{walletId}/invoices/{invoiceId}/owned-cards/reveal.
وتستخدم قراءة بطاقة الكتالوج GET /v1/wallets/{walletId}/catalog/cards/{cardId}.
تُستخدم CardSummary في البطاقات المملوكة والكشف وsoldCards في طلب الشراء: معرّف البطاقة واسمها المحلي،
والقيمة المطبوعة والشعار الاختياريان، والفئة الفرعية والفئة. تستخدم البطاقة المملوكة soldCardId وoperationId.
وتشمل البطاقة المكشوفة داخل طلب الشراء معرّف الفاتورة وملخص البطاقة ووقت الشراء أيضًا. تُحذف النصوص الفارغة
والنصوص المحلية الفارغة. المال نص بثلاث خانات عشرية ورمز عملة من ثلاثة أحرف كبيرة — ولا يكون سالبًا إلا رصيد المحفظة حين تستخدم الدين المسموح؛ اقرأ عملة المحفظة
من balance.currency.
تشترط الحزمة وسم التوقيع v2، وترويستي توقيع الاستدعاء في قائمة الأجزاء، وفحوص دورة حياة المفاتيح المنشورة. راجع التحقق من الردود. يسري الإيقاف فورًا؛ ولا يستأنف التطبيق إلا موظفو أنيس، وإلغاء مفتاحك الحالي يتطلب تسجيلًا جديدًا. راجع الأمان.
var reason = "Suspected compromise";
var catalogueCard = await client.Catalogue.GetCardAsync(walletId, cardId);
var suspended = await client.Access.SuspendApplicationAsync(reason);
var revoked = await client.Access.RevokeKeyAsync(keyId, reason);
موارد المنتجات في SDK 2.2.0
يضيف الإصدار 2.2.0 من حزم SDK meterCharges وdirectRechargeProducts (المنتجات والمواقع وحساب السعر) وdirectRecharges (الإنشاء وقائمة عملياتك والقراءة وتعديل المستلم) وwebhookEndpoint وwebhookEvents وأداة للتحقق من توقيع الإشعارات وأنواع الأخطاء المحدّثة. حدّث من الإصدار 2.1.0 لاستخدامها. راجع أدلة العدادات والشحن المباشر والإشعارات لمعرفة سلوك HTTP. احفظ معرّف العملية ومحتواها كما سترسله قبل أي استدعاء ينقل مالًا؛ واستعد الردّ الضائع بالمعرّف نفسه. يذكر ملف README في كل حزمة وملف docs/webhooks.md فيها أسماء الدوال ومعاملات أداة التحقق.