using System.Globalization; using DevExpress.Office; using DevExpress.XtraRichEdit; using DevExpress.XtraRichEdit.API.Native; namespace ROLAC.API.Services.Disbursement; /// /// Renders a check on 8.5"x11" stock using the DevExpress Office (RichEdit) API: /// a check block on top followed by two identical ledger detail stubs. The layout is /// built programmatically (no external .docx template) and exported to PDF. /// public class CheckPrintService : ICheckPrintService { public Task RenderPdfAsync(CheckPrintModel model) { using var server = new RichEditDocumentServer(); var doc = server.Document; doc.BeginUpdate(); try { doc.Unit = DocumentUnit.Inch; var section = doc.Sections[0]; section.Page.Width = 8.5f; section.Page.Height = 11f; section.Margins.Left = section.Margins.Right = 0.6f; section.Margins.Top = section.Margins.Bottom = 0.5f; BuildCheckBlock(doc, model); BuildStub(doc, model, "PAYMENT ADVICE — DETAIL"); BuildStub(doc, model, "PAYMENT ADVICE — RECORD COPY"); } finally { doc.EndUpdate(); } var ms = new MemoryStream(); server.ExportToPdf(ms); ms.Position = 0; return Task.FromResult(ms); } public Task RenderReceiptPdfAsync(CheckPrintModel model) { using var server = new RichEditDocumentServer(); var document = server.Document; document.BeginUpdate(); try { document.Unit = DocumentUnit.Inch; var section = document.Sections[0]; section.Page.Width = 8.5f; section.Page.Height = 11f; section.Margins.Left = section.Margins.Right = 0.8f; section.Margins.Top = section.Margins.Bottom = 0.8f; document.AppendHtmlText(BuildReceiptHtml(model)); } finally { document.EndUpdate(); } var stream = new MemoryStream(); server.ExportToPdf(stream); stream.Position = 0; return Task.FromResult(stream); } private static string BuildReceiptHtml(CheckPrintModel model) { var issuer = model.Issuer; var check = model.Check; var issuerAddress = Encode(JoinAddress(issuer.Address, issuer.City, issuer.State, issuer.ZipCode)); var payeeAddress = Encode(JoinAddress(check.PayeeAddress, check.PayeeCity, check.PayeeState, check.PayeeZip)); var detailRows = new System.Text.StringBuilder(); foreach (var line in model.Lines) { var date = line.Expense?.ExpenseDate.ToString("MM/dd/yyyy") ?? ""; detailRows.Append( "" + $"{Encode(date)}" + $"{Encode(line.Description)}" + $"{Encode(FormatCurrency(line.Amount))}" + ""); } var signedOn = check.ReceiptSignedAt.HasValue ? check.ReceiptSignedAt.Value.ToLocalTime().ToString("MM/dd/yyyy HH:mm") : ""; var signatureBlock = ""; if (model.SignatureImage is { Length: > 0 }) { var base64 = Convert.ToBase64String(model.SignatureImage); signatureBlock = $"

"; } return "
" + "

Disbursement Receipt / 簽收收據

" + $"

{Encode(issuer.Name)}
{issuerAddress}

" + "

Check Information / 支票資訊

" + "" + $"" + $"" + $"" + $"" + $"" + "
Check No. / 支票號碼{Encode(check.CheckNumber)}Date / 日期{check.CheckDate:MM/dd/yyyy}
Payee / 收款人{Encode(check.PayeeName)} {payeeAddress}
Amount / 金額{Encode(FormatCurrency(check.Amount))} — {Encode(model.AmountInWords)}
Memo / 摘要{Encode(check.Memo ?? "")}
" + "

Disbursement Detail / 撥款明細

" + "" + "" + detailRows + $"" + "
Date / 日期Description / 說明Amount / 金額
TOTAL / 合計{Encode(FormatCurrency(check.Amount))}
" + "

Acknowledgement of Receipt / 收款簽收

" + "

I acknowledge receipt of the above payment in full. / 本人確認已如數收到上述款項。

" + $"

Received by / 簽收人: {Encode(check.ReceiptSignedName ?? "")}
" + $"Date / 簽收日期: {Encode(signedOn)}

" + signatureBlock + "
"; } private static string Encode(string? text) => System.Net.WebUtility.HtmlEncode(text ?? ""); private static void BuildCheckBlock(Document doc, CheckPrintModel m) { var issuer = m.Issuer; var check = m.Check; AppendLine(doc, issuer.Name, bold: true, size: 13); var issuerAddr = JoinAddress(issuer.Address, issuer.City, issuer.State, issuer.ZipCode); if (!string.IsNullOrWhiteSpace(issuerAddr)) AppendLine(doc, issuerAddr, size: 9); if (!string.IsNullOrWhiteSpace(issuer.BankName)) AppendLine(doc, issuer.BankName, size: 9); AppendLine(doc, ""); AppendLine(doc, $"Check No: {check.CheckNumber} Date: {check.CheckDate:MM/dd/yyyy}", bold: true, size: 10); AppendLine(doc, ""); AppendLine(doc, $"PAY TO THE ORDER OF: {check.PayeeName}", bold: true, size: 11); var payeeAddr = JoinAddress(check.PayeeAddress, check.PayeeCity, check.PayeeState, check.PayeeZip); if (!string.IsNullOrWhiteSpace(payeeAddr)) AppendLine(doc, payeeAddr, size: 9); AppendLine(doc, ""); AppendLine(doc, $"AMOUNT: {FormatCurrency(check.Amount)}", bold: true, size: 12); AppendLine(doc, m.AmountInWords, size: 10); if (!string.IsNullOrWhiteSpace(check.Memo)) { AppendLine(doc, ""); AppendLine(doc, $"Memo: {check.Memo}", size: 9); } AppendLine(doc, ""); AppendLine(doc, "____________________________________", size: 10); AppendLine(doc, "Authorized Signature", size: 8); AppendSeparator(doc); } private static void BuildStub(Document doc, CheckPrintModel m, string title) { var check = m.Check; AppendLine(doc, title, bold: true, size: 10); AppendLine(doc, $"Check No: {check.CheckNumber} Date: {check.CheckDate:MM/dd/yyyy} Payee: {check.PayeeName}", size: 9); AppendLine(doc, ""); var rows = m.Lines.Count + 2; // header + lines + total var table = doc.Tables.Create(doc.Range.End, rows, 3, AutoFitBehaviorType.AutoFitToWindow); table.Borders.InsideHorizontalBorder.LineStyle = TableBorderLineStyle.Single; table.Borders.Top.LineStyle = table.Borders.Bottom.LineStyle = TableBorderLineStyle.Single; SetCell(doc, table[0, 0], "Date", bold: true); SetCell(doc, table[0, 1], "Description", bold: true); SetCell(doc, table[0, 2], "Amount", bold: true, right: true); for (var i = 0; i < m.Lines.Count; i++) { var line = m.Lines[i]; var r = i + 1; // CheckLine snapshots description; date comes from the source expense if loaded. var date = line.Expense?.ExpenseDate.ToString("MM/dd/yyyy") ?? ""; SetCell(doc, table[r, 0], date); SetCell(doc, table[r, 1], line.Description); SetCell(doc, table[r, 2], FormatCurrency(line.Amount), right: true); } var totalRow = rows - 1; SetCell(doc, table[totalRow, 0], ""); SetCell(doc, table[totalRow, 1], "TOTAL", bold: true, right: true); SetCell(doc, table[totalRow, 2], FormatCurrency(check.Amount), bold: true, right: true); AppendLine(doc, ""); if (check.ReceiptSignedAt is { } signedAt) AppendLine(doc, $"Received by: {check.ReceiptSignedName} on {signedAt:MM/dd/yyyy HH:mm}", size: 9); AppendSeparator(doc); } // ── low-level helpers ────────────────────────────────────────────────────── private static void AppendLine(Document doc, string text, bool bold = false, float size = 10) { var range = doc.AppendText(text + "\r\n"); var cp = doc.BeginUpdateCharacters(range); cp.Bold = bold; cp.FontSize = size; doc.EndUpdateCharacters(cp); } private static void AppendSeparator(Document doc) { AppendLine(doc, ""); AppendLine(doc, "------------------------------------------------------------------------------------------", size: 8); AppendLine(doc, ""); } private static void SetCell(Document doc, TableCell cell, string text, bool bold = false, bool right = false) { var range = doc.InsertText(cell.ContentRange.Start, text); var cp = doc.BeginUpdateCharacters(range); cp.Bold = bold; cp.FontSize = 9; doc.EndUpdateCharacters(cp); if (right) { var pp = doc.BeginUpdateParagraphs(range); pp.Alignment = ParagraphAlignment.Right; doc.EndUpdateParagraphs(pp); } } private static string FormatCurrency(decimal amount) => amount.ToString("C2", CultureInfo.GetCultureInfo("en-US")); private static string JoinAddress(string? addr, string? city, string? state, string? zip) { var cityLine = string.Join(", ", new[] { city, string.Join(" ", new[] { state, zip }.Where(s => !string.IsNullOrWhiteSpace(s))) } .Where(s => !string.IsNullOrWhiteSpace(s))); return string.Join(" ", new[] { addr, cityLine }.Where(s => !string.IsNullOrWhiteSpace(s))); } }