using SendGrid;
using [Link];
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using Twilio;
using [Link];
public interface IOtpService
{
(string code, string hash, string salt, DateTime expiresAt) Generate();
Task SendEmailAsync(string toEmail, string code);
Task SendSmsAsync(string toPhone, string code);
bool Verify(string code, string hash, string salt);
}
public class OtpService : IOtpService
{
private const int Length = 6;
private const int Minutes = 5;
public (string code, string hash, string salt, DateTime expiresAt) Generate()
{
// C# 7.3: classic using block
using (var rng = [Link]())
{
var bytes = new byte[4];
[Link](bytes);
var value = BitConverter.ToUInt32(bytes, 0) % 900000 + 100000; //
100000..999999
var code = [Link]();
var salt = [Link]().ToString("N");
var hash = Hash(code, salt);
return (code, hash, salt, [Link](Minutes));
}
}
public bool Verify(string code, string hash, string salt)
{
return Hash(code, salt) == hash;
}
private static string Hash(string code, string salt)
{
// C# 7.3: classic using block
using (var sha = [Link]())
{
var data = [Link](code + "|" + salt);
return Convert.ToBase64String([Link](data));
}
}
public async Task SendEmailAsync(string toEmail, string code)
{
var apiKey = [Link]["SENDGRID_API_KEY"];
var from = [Link]["SENDGRID_FROM_EMAIL"] ?? "no-
reply@[Link]";
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage
{
From = new EmailAddress(from, "ASFAAR"),
Subject = "Your OTP Code",
PlainTextContent = $"Your verification code is: {code} (expires in
{Minutes} minutes).",
HtmlContent = $"<p>Your verification code is: <b>{code}</b> (expires in
{Minutes} minutes).</p>"
};
[Link](new EmailAddress(toEmail));
await [Link](msg);
}
public async Task SendSmsAsync(string toPhone, string code)
{
[Link](
[Link]["TWILIO_ACCOUNT_SID"],
[Link]["TWILIO_AUTH_TOKEN"]
);
await [Link](
to: toPhone,
from: [Link]["TWILIO_FROM_NUMBER"],
body: $"Your verification code is {code}. It expires in {Minutes}
minutes."
);
}
}
using System;
using [Link];
using [Link];
using [Link];
namespace ASFAAR_V1.Models
{
public class OtpToken
{
public int OtpTokenId { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Purpose { get; set; } // e.g., "Register"
public string CodeHash { get; set; } // store hash, not raw
public DateTime ExpiresUtc { get; set; }
public int Attempts { get; set; }
public string SessionId { get; set; }
public DateTime CreatedUtc { get; set; }
}
}:
::::::::::::::::::::::::::::::::::::::::::::
// Services/Payments/[Link]
using System;
using [Link];
public interface IPaymentGatewayFactory
{
IPaymentGateway Resolve(string key); // "paystack" | "flutterwave" | "fincra"
}
public class PaymentGatewayFactory : IPaymentGatewayFactory
{
private readonly Dictionary<string, IPaymentGateway> _map;
public PaymentGatewayFactory()
{
_map = new Dictionary<string,
IPaymentGateway>([Link])
{
["paystack"] = new PaystackGateway(),
// ["flutterwave"] = new FlutterwaveGateway(),
// ["fincra"] = new FincraGateway(),
};
}
public IPaymentGateway Resolve(string key)
=> _map.TryGetValue(key ?? "", out var g) ? g : null;
}
// Services/[Link]
using ASFAAR_V1.Models;
using [Link];
using [Link];
using [Link];
using [Link]; // keep WinForms LocalReport
using SendGrid;
using [Link];
using System;
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using [Link];
using ZXing;
using [Link];
using [Link];
namespace ASFAAR_V1.Services
{
public sealed class PaymentOrchestrator
{
private readonly ApplicationDbContext _db;
private readonly IPaymentGatewayFactory _factory;
public PaymentOrchestrator(ApplicationDbContext db, IPaymentGatewayFactory
factory)
{
_db = db;
_factory = factory;
}
private static byte[] GenerateBarcodePng(string text, int width = 500, int
height = 120, int margin = 2, bool pure = true)
{
if ([Link](text)) return null;
var writer = new BarcodeWriter
{
Format = BarcodeFormat.CODE_128,
Options = new EncodingOptions
{
Width = width,
Height = height,
Margin = margin,
PureBarcode = pure
},
Renderer = new BitmapRenderer()
};
using (var bitmap = [Link](text))
using (var ms = new MemoryStream())
{
[Link](ms, [Link]);
return [Link]();
}
}
private static byte[] GenerateQrCodePng(string text, int pixelsPerModule =
6, int margin = 2)
{
if ([Link](text)) return null;
// For QR we specify only width/height via EncodingOptions; ZXing will
square it
var size = [Link](128, pixelsPerModule * 33); // rough base size
var writer = new BarcodeWriter
{
Format = BarcodeFormat.QR_CODE,
Options = new EncodingOptions
{
Width = size,
Height = size,
Margin = margin
},
Renderer = new BitmapRenderer()
};
using (var bitmap = [Link](text))
using (var ms = new MemoryStream())
{
[Link](ms, [Link]);
return [Link]();
}
}
// Optional: load applicant photo bytes (from ~/ path or absolute URL)
private static byte[] TryLoadImageBytes(string pathOrUrl)
{
try
{
if ([Link](pathOrUrl)) return null;
// Absolute URL
if ([Link](pathOrUrl, [Link]))
{
using (var wc = new WebClient())
return [Link](pathOrUrl);
}
// App-relative path (~/Uploads/..., /Uploads/..., etc.)
var virt = [Link]("~") ? pathOrUrl : "~/" +
[Link]('/');
var abs = [Link](virt);
if ( && [Link](abs))
return [Link](abs);
}
catch { /* swallow */ }
return null;
}
// ===== START: initialize gateway + record a PaymentTransaction =====
public async Task<PaymentInitResponse> StartAsync(Guid token, string
gateway, string callbackUrl)
{
var now = [Link];
var hold = await _db.BiometricSlotHolds
.Include(h => [Link])
.Include(h => [Link])
.FirstOrDefaultAsync(h => [Link] == token && ![Link] &&
[Link] > now);
if (hold == null)
return new PaymentInitResponse { Ok = false, Message = "Reservation
not found/expired." };
var va = [Link];
var pi = va?.IntPassport;
var customerName = [Link](" ", new[] { pi?.Surname, pi?.Firstname,
pi?.Othername }.Where(x => ));
var customerEmail = va?.ApplicationEmail;
var reference = $"BIO-{[Link]:yyyyMMddHHmmss}-
{[Link]("N").Substring(0, 8)}";
var gw = _factory.Resolve(gateway);
if (gw == null)
return new PaymentInitResponse { Ok = false, Message =
$"Unsupported gateway '{gateway}'." };
var initReq = new PaymentInitRequest
{
Reference = reference,
Amount = [Link],
Currency = "NGN",
Email = customerEmail,
CallbackUrl = callbackUrl
};
var init = await [Link](initReq);
// log transaction
var txn = new PaymentTransaction
{
VisaApplicationId = va?.VisaApplicationId,
BiometricSlotId = [Link],
HoldToken = [Link],
Gateway = gateway,
Reference = reference,
Status = [Link] ? "initialized" : "init_failed",
Amount = [Link],
Currency = "NGN",
CustomerEmail = customerEmail,
CustomerName = customerName,
CreatedUtc = [Link],
RawInitializeResponse = [Link]
};
_db.[Link](txn);
// store on HOLD so callback can match
[Link] = gateway;
[Link] = reference;
await _db.SaveChangesAsync();
return init;
}
// ===== VERIFY + FINALIZE + PDFs + EMAIL =====
public async Task<(bool ok, int appointmentId, string message)>
VerifyAndFinalizeAsync(
string gateway, string reference, string aspNetUserId)
{
var gw = _factory.Resolve(gateway);
if (gw == null) return (false, 0, $"Unsupported gateway '{gateway}'.");
var verify = await [Link](reference);
// Update transaction row
var txn = await _db.[Link](t =>
[Link] == reference);
if (txn != null)
{
[Link] = [Link] ? "success" : "failed";
[Link] = [Link] ?? [Link];
[Link] = [Link] ? (DateTime?)[Link] : null;
await _db.SaveChangesAsync();
}
if (![Link]) return (false, 0, "Gateway reports payment not
successful.");
// Find the hold we created
var hold = await _db.BiometricSlotHolds
.Include(h => [Link])
.Include(h => [Link])
.Include(h => [Link])
.FirstOrDefaultAsync(h => ![Link] && [Link] ==
reference);
if (hold == null) return (false, 0, "Reservation not found.");
// Finalize -> create BiometricAppointment and mark hold confirmed
var apptId = await FinalizeFromHoldAsync(hold, gateway, reference,
aspNetUserId);
// Finish transaction linkage + invoice number
if (txn != null)
{
[Link] = apptId;
[Link] = await GenerateNextInvoiceNumberAsync();
await _db.SaveChangesAsync();
}
// ===== ENQUEUE (auto add to queue after successful booking) =====
try
{
await new ASFAAR_V1.[Link](_db)
.EnsureTicketForAppointmentAsync(apptId);
// Optional: if you want to store the ticket no. on the txn (add a
column first)
// if (txn != null && [Link] && !
[Link]([Link]))
// {
// [Link] = [Link];
// await _db.SaveChangesAsync();
// }
// If enqueue failed, we continue anyway (booking succeeded).
// You can log [Link] here if needed.
}
catch
{
// swallow queue errors; do not fail the booking
}
// Build PDFs from strongly-typed VMs + send email (unchanged)
try
{
var outputs = await BuildOutputsForAppointmentAsync(apptId,
txn?.InvoiceNumber, reference, gateway);
var invoicePdf = [Link];
var bookingPdf = [Link];
var combinedPdf = [Link];
var emailHtml = [Link];
await SendEmailWithAttachmentsAsync(
to: txn?.CustomerEmail,
subject: "Biometric appointment confirmed",
htmlBody: emailHtml,
attachments: new[]
{
($"Invoice-{txn?.InvoiceNumber ?? [Link]()}.pdf",
invoicePdf),
($"Booking-{apptId}.pdf",
bookingPdf),
($"ASFAAR-{txn?.InvoiceNumber ?? "Booking"}-{apptId}.pdf",
combinedPdf)
}
);
}
catch
{
// Do not fail booking if email/report fails
}
return (true, apptId, null);
}
private async Task<int> FinalizeFromHoldAsync(BiometricSlotHold hold,
string gateway, string reference, string aspNetUserId)
{
int apptId;
using (var tx =
_db.[Link]([Link]))
{
var affected = await _db.[Link](
@"UPDATE [Link]
SET BookedCount = BookedCount + 1
WHERE BiometricSlotId = @p0
AND IsOpen = 1
AND BookedCount < Capacity",
[Link]);
if (affected == 0)
{
[Link]();
throw new InvalidOperationException("Slot became full during
confirmation.");
}
Guid uid = [Link];
if ()
{
uid = await _db.[Link](u => [Link] == aspNetUserId)
.Select(u => [Link])
.FirstOrDefaultAsync();
}
var appt = new BiometricAppointment
{
BiometricSlotId = [Link],
VisaApplicationId = [Link],
BiometricCategoryId = [Link] ?? 0,
IsPaid = true,
IsCompleted = false,
IsCaptured = false,
IsAttended = false,
UserIdN = uid,
UpdateUserIdN = uid,
Timestamp = [Link],
UpdateTimestamp = [Link]
};
_db.[Link](appt);
[Link] = true;
[Link] = gateway;
[Link] = reference;
await _db.SaveChangesAsync();
[Link]();
apptId = [Link];
}
return apptId;
}
private static string Fmt12h(TimeSpan t)
=> [Link](t).ToString("hh:mm tt",
[Link]);
// ===== Build VMs -> render 2 PDFs -> combine -> HTML email =====
private async Task<(byte[] invoicePdf, byte[] bookingPdf, byte[]
combinedPdf, string emailHtml)>
BuildOutputsForAppointmentAsync(int appointmentId, string
invoiceNumber, string reference, string gateway)
{
var appt = await _db.BiometricAppointments
.Include(a => [Link])
.Include(a => [Link])
.Include(a => [Link])
.Include(a => [Link])
.FirstOrDefaultAsync(a => [Link] ==
appointmentId);
if (appt == null) return (null, null, null, null);
var center = await _db.BiometricCenters
.Where(c => [Link] == [Link])
.Select(c => new { [Link], [Link] })
.FirstOrDefaultAsync();
var amount = await _db.BiometricSlotHolds
.Where(h => [Link]
&& [Link] == [Link]
&& [Link] == [Link])
.OrderByDescending(h => [Link])
.Select(h => (decimal?)[Link])
.FirstOrDefaultAsync() ?? 0m;
var pi = [Link]?.IntPassport;
var applicantName = [Link](" ", new[] { pi?.Surname,
pi?.Firstname, pi?.Othername }.Where(x => ));
var applicantEmail = [Link]?.ApplicationEmail;
var slotLabel = $"{Fmt12h([Link])} –
{Fmt12h([Link])}";
// ----- Build barcode/QR payloads + applicant photo bytes -----
var qrPayload = $"ASFAAR|APP:{[Link]?.ApplicationNo}|
APPT:{appointmentId}|INV:{invoiceNumber}|REF:{reference}";
var barcodeValue = reference; // keep barcode concise & scannable
var qrBytes = GenerateQrCodePng(qrPayload, pixelsPerModule: 6, margin:
2);
var barcodeBytes = GenerateBarcodePng(barcodeValue, width: 500, height:
120, margin: 2, pure: true);
// Applicant photo -> byte[] if you want to show it on the invoice
byte[] applicantPhotoBytes =
TryLoadImageBytes([Link]?.ApplicantPhoto);
// === 1) Build VMs (strongly-typed) ===
var invoiceRows = new[]
{
new InvoiceRowVM
{
InvoiceNumber = invoiceNumber,
Reference = reference,
Gateway = gateway,
Amount = amount,
Currency = "NGN",
ApplicantName = applicantName,
ApplicantEmail = applicantEmail,
ApplicationNo = [Link]?.ApplicationNo,
VisaType = [Link]?.VisaType?.VisaName,
Center = center?.Name,
CenterAddress = center?.Address,
ApptDate = [Link],
Slot = slotLabel,
Category = [Link]?.Name,
// NEW:
BarcodeImage = barcodeBytes,
QRcodeImage = qrBytes,
ApplicantPhoto = applicantPhotoBytes,
IssuedAt = [Link]
}
};
var bookingRows = new[]
{
new BookingRowVM
{
ApplicationNo = [Link]?.ApplicationNo,
ApplicantName = applicantName,
ApplicantEmail = applicantEmail,
VisaType = [Link]?.VisaType?.VisaName,
Center = center?.Name,
CenterAddress = center?.Address,
ApptDate = [Link],
Slot = slotLabel,
Category = [Link]?.Name,
// NEW:
BarcodeImage = barcodeBytes,
QRcodeImage = qrBytes
}
};
// === 2) Render PDFs from RDLCs with the VMs ===
var invoicePdf = RenderRdlc("~/Reports/[Link]",
("InvoiceDS", invoiceRows));
var bookingPdf = RenderRdlc("~/Reports/[Link]",
("BookingDS", bookingRows));
var combinedPdf = CombinePdfs(invoicePdf, bookingPdf);
// === 3) Branded HTML ===
var html = BuildBrandedHtmlEmail(
appName: [Link]["Brand:AppName"] ??
"ASFAAR",
logoUrl: [Link]["Brand:LogoUrl"] ??
"[Link]
supportEmail:
[Link]["Brand:SupportEmail"] ?? "support@[Link]",
name: applicantName,
applicationNo: [Link]?.ApplicationNo,
visaType: [Link]?.VisaType?.VisaName,
centerName: center?.Name,
centerAddress: center?.Address,
apptDate: [Link],
slot: slotLabel,
invoiceNo: invoiceNumber,
gateway: gateway,
reference: reference,
amountMajor: amount
);
return (invoicePdf, bookingPdf, combinedPdf, html);
}
// ===== LocalReport Renderer (IEnumerable<T> datasets) =====
private static byte[] RenderRdlc(string rdlcVirtualPath, params (string
name, object data)[] datasets)
{
var path = [Link](rdlcVirtualPath);
if ([Link](path) || )
throw new FileNotFoundException("RDLC not found", rdlcVirtualPath);
var lr = new LocalReport { ReportPath = path };
[Link]();
foreach (var (name, data) in datasets ?? [Link]<(string,
object)>())
[Link](new ReportDataSource(name, data));
string mime, enc, ext;
Warning[] warnings;
string[] streamIds;
return [Link]("PDF", null, out mime, out enc, out ext, out
streamIds, out warnings);
}
private static byte[] CombinePdfs(params byte[][] pdfs)
{
using (var output = new MemoryStream())
{
var doc = new Document();
var copy = new PdfCopy(doc, output);
[Link]();
try
{
foreach (var pdf in pdfs ?? [Link]<byte[]>())
{
if (pdf == null || [Link] == 0) continue;
var reader = new PdfReader(pdf);
try
{
for (int i = 1; i <= [Link]; i++)
[Link]([Link](reader, i));
}
finally
{
[Link]();
}
}
}
finally
{
[Link]();
}
return [Link]();
}
}
private async Task<string> GenerateNextInvoiceNumberAsync()
{
var prefix = "INV-" + [Link]("yyyyMM") + "-";
var last = await _db.PaymentTransactions
.Where(t => [Link](prefix))
.OrderByDescending(t => [Link])
.Select(t => [Link])
.FirstOrDefaultAsync();
var nextSeq = 1;
if ()
{
var tail = [Link]([Link]);
if ([Link](tail, out var n)) nextSeq = n + 1;
}
return prefix + [Link]("D6");
}
private static string BuildBrandedHtmlEmail(
string appName, string logoUrl, string supportEmail,
string name, string applicationNo, string visaType,
string centerName, string centerAddress, DateTime apptDate, string
slot,
string invoiceNo, string gateway, string reference, decimal
amountMajor)
{
string Kv(string k, string v)
=> $"<tr><td style=\"padding:8px 0;color:#64748b;font-
size:13px;\">{[Link](k)}</td><td style=\"padding:8px
0;text-align:right;font-weight:600;color:#0f172a;font-
size:13px;\">{[Link](v ?? "—")}</td></tr>";
var sb = new StringBuilder();
[Link]("<!doctype html><html lang=\"en\"><head>");
[Link](" <meta charset=\"utf-8\"><meta name=\"viewport\"
content=\"width=device-width, initial-scale=1.0\">");
[Link]($" <title>{[Link](appName)} •
Booking confirmed</title>");
[Link]("
<style>body{margin:0;padding:24px;background:#f5f7fb;font-family:Segoe
UI,Arial,sans-serif}.card{background:#fff;border-radius:16px;box-shadow:0 12px 28px
rgba(0,0,0,.06);overflow:hidden}.muted{color:#64748b}.kv{width:100%;border-
collapse:collapse}</style>");
[Link]("</head><body><div style=\"max-width:640px;margin:0
auto;\">");
[Link](" <table role=\"presentation\" width=\"100%\"
cellspacing=\"0\" cellpadding=\"0\" style=\"margin-bottom:16px;\">");
[Link](" <tr><td style=\"text-align:left;\">");
[Link]($" <img
src=\"{[Link](logoUrl)}\" height=\"40\"
alt=\"{[Link](appName)}\" style=\"display:block\">");
[Link](" </td></tr></table>");
[Link](" <table role=\"presentation\" width=\"100%\"
cellspacing=\"0\" cellpadding=\"0\" class=\"card\">");
[Link](" <tr><td style=\"padding:28px 28px 8px 28px;\">");
[Link](" <h2 style=\"margin:0 0 8px 0;font-
size:22px;color:#0f172a;\">Biometric appointment confirmed</h2>");
[Link]($" <p class=\"muted\" style=\"margin:0;\">Thanks,
<strong>{[Link](name ?? "Applicant")}</strong>. Your
booking and invoice are attached.</p>");
[Link](" </td></tr>");
[Link](" <tr><td style=\"padding:8px 28px;\">");
[Link](" <table class=\"kv\">");
[Link](Kv("Application no.", applicationNo));
[Link](Kv("Visa type", visaType));
[Link](Kv("Center", centerName));
if ()
[Link](Kv("Address", centerAddress));
[Link](Kv("Date", [Link]("dd MMM yyyy")));
[Link](Kv("Time", slot));
[Link](Kv("Category", "Biometric"));
if () [Link](Kv("Invoice",
invoiceNo));
[Link](Kv("Gateway", gateway?.ToUpperInvariant()));
[Link](Kv("Payment ref", reference));
[Link](Kv("Amount", $"NGN {amountMajor:N2}"));
[Link](" </table>");
[Link](" </td></tr>");
[Link](" <tr><td style=\"padding:16px 28px 28px 28px;\">");
[Link]($" <p class=\"muted\" style=\"margin:12px 0 0
0;\">Need help? <a href=\"mailto:
{[Link](supportEmail)}\">Contact
support</a></p>");
[Link](" </td></tr>");
[Link](" </table>");
[Link]($" <p class=\"muted\" style=\"text-
align:center;margin:14px 0 0;\">© {[Link]}
{[Link](appName)}. All rights reserved.</p>");
[Link]("</div></body></html>");
return [Link]();
}
private static async Task SendEmailWithAttachmentsAsync(
string to, string subject, string htmlBody,
(string fileName, byte[] bytes)[] attachments)
{
var apiKey = [Link]["SENDGRID_API_KEY"];
var from = [Link]["SENDGRID_FROM_EMAIL"] ??
"no-reply@[Link]";
if ([Link](apiKey) || [Link](to))
return;
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage
{
From = new EmailAddress(from, "ASFAAR"),
Subject = subject,
HtmlContent = htmlBody,
PlainTextContent =
[Link](htmlBody ?? "", "<.*?>", [Link])
};
[Link](new EmailAddress(to));
if (attachments != null)
{
foreach (var att in attachments)
{
if ([Link] == null || [Link] == 0) continue;
[Link]([Link],
Convert.ToBase64String([Link]), "application/pdf");
}
}
await [Link](msg);
}
}
}
using System;
using [Link];
using [Link];
using [Link];
using [Link];
namespace ASFAAR_V1.Models
{
public class PaymentTransaction
{
[Key]
public Guid PaymentTransactionId { get; set; } = [Link]();
// Linkage
public Guid? VisaApplicationId { get; set; }
public int? BiometricAppointmentId { get; set; }
public Guid? HoldToken { get; set; } // [Link]
(Guid)
public int? BiometricSlotId { get; set; }
// Gateway metadata
[MaxLength(40)] public string Gateway { get; set; } // e.g.
"paystack"
[MaxLength(100)] public string Reference { get; set; } // your
reference used for charge
[MaxLength(50)] public string Status { get; set; } //
initialized|success|failed
[MaxLength(10)] public string Currency { get; set; } // NGN
// Money
public decimal Amount { get; set; } // major
units
// Customer
[MaxLength(200)] public string CustomerEmail { get; set; }
[MaxLength(200)] public string CustomerName { get; set; }
// Timestamps
public DateTime CreatedUtc { get; set; } = [Link];
public DateTime? PaidAtUtc { get; set; }
// Extra details (optional)
[MaxLength(50)] public string AuthorizationCode { get; set; }
[MaxLength(50)] public string Channel { get; set; } // e.g. "card"
[MaxLength(40)] public string CardBrand { get; set; } // "visa"
[MaxLength(50)] public string CountryCode { get; set; }
[MaxLength(64)] public string IPAddress { get; set; }
public decimal? Fees { get; set; }
// Free-form storage
public string RawInitializeResponse { get; set; } // JSON/text
public string RawVerifyResponse { get; set; } // JSON/text
public string MetaJson { get; set; } // JSON you want to keep
// Invoicing
[MaxLength(40)] public string InvoiceNumber { get; set; } // e.g. INV-
202510-000123
}
}
// Services/Payments/[Link]
using [Link];
using System;
using [Link];
using [Link];
using [Link];
using [Link];
public class PaystackGateway : IPaymentGateway
{
private readonly PayStackApi _api;
private readonly string _currency;
public string Key => "paystack";
public PaystackGateway()
{
var secret = [Link]["Paystack:SecretKey"];
_currency = [Link]["Paystack:Currency"] ?? "NGN";
_api = new PayStackApi(secret);
}
private static int ToKobo(decimal amountMajor)
{
var kobo = [Link](amountMajor * 100m, 0,
[Link]);
if (kobo < 100m) kobo = 100m;
if (kobo > [Link]) throw new
ArgumentOutOfRangeException(nameof(amountMajor), "Amount too large for Paystack.");
return (int)kobo;
}
public Task<PaymentInitResponse> InitializeAsync(PaymentInitRequest req)
{
var metaObj = ([Link] ?? new Dictionary<string, string>())
.ToDictionary(kv => [Link], kv => (object)[Link]);
var init = new TransactionInitializeRequest
{
AmountInKobo = ToKobo([Link]),
Email = [Link],
Reference = [Link],
CallbackUrl = [Link],
Currency = [Link]([Link]) ? _currency :
[Link],
// IMPORTANT: carry your hold token + any other context
MetadataObject = metaObj
};
var resp = _api.[Link](init);
var ok = resp != null && [Link] && [Link] != null && !
[Link]([Link]);
return [Link](new PaymentInitResponse
{
Ok = ok,
AuthorizationUrl = resp?.Data?.AuthorizationUrl,
Reference = resp?.Data?.Reference ?? [Link],
Message = resp?.Message
});
}
public Task<PaymentVerifyResponse> VerifyAsync(string reference)
{
var v = _api.[Link](reference);
var paid = v != null && [Link] && [Link] != null
&& [Link]([Link], "success",
[Link]);
return [Link](new PaymentVerifyResponse
{
Paid = paid,
Reference = reference,
RawStatus = v?.Data?.Status ?? v?.Message,
Message = v?.Message
});
}
}
using [Link];
using [Link];
public class PaymentInitRequest
{
public string Reference { get; set; }
public decimal Amount { get; set; } // in major units (e.g., NGN)
public string Currency { get; set; } // e.g., "NGN"
public string Email { get; set; } // customer email
public string CallbackUrl { get; set; } // absolute URL
public Dictionary<string, string> Metadata { get; set; } = new
Dictionary<string, string>();
}
public class PaymentInitResponse
{
public bool Ok { get; set; }
public string AuthorizationUrl { get; set; }
public string Reference { get; set; }
public string Message { get; set; }
}
public class PaymentVerifyResponse
{
public bool Paid { get; set; }
public string Reference { get; set; }
public string RawStatus { get; set; } // gateway raw status string
public string Message { get; set; }
}
public interface IPaymentGateway
{
string Key { get; } // "paystack", "flutterwave", "fincra"
Task<PaymentInitResponse> InitializeAsync(PaymentInitRequest req);
Task<PaymentVerifyResponse> VerifyAsync(string reference);
}
// Payments/[Link]
using System;
using [Link];
using [Link];
using [Link];
public interface IPaymentService
{
Task<PaymentInitResponse> InitializeAsync(string gateway, PaymentInitRequest
req);
Task<PaymentVerifyResponse> VerifyAsync(string gateway, string reference);
}
public sealed class PaymentService : IPaymentService
{
private readonly Dictionary<string, IPaymentGateway> _map;
public PaymentService(IEnumerable<IPaymentGateway> gateways)
{
// case-insensitive map by Key
_map = gateways?.ToDictionary(g => [Link], [Link])
?? new Dictionary<string,
IPaymentGateway>([Link]);
}
private IPaymentGateway Resolve(string gateway)
{
if ([Link](gateway) || !_map.TryGetValue(gateway, out
var g))
throw new ArgumentException($"Unsupported or missing gateway
'{gateway}'.");
return g;
}
public Task<PaymentInitResponse> InitializeAsync(string gateway,
PaymentInitRequest req)
=> Resolve(gateway).InitializeAsync(req);
public Task<PaymentVerifyResponse> VerifyAsync(string gateway, string
reference)
=> Resolve(gateway).VerifyAsync(reference);
}