From 52db924e06263498476128da15d63c87a1007ea4 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Thu, 18 Dec 2025 08:18:49 -0500 Subject: [PATCH 1/3] Add Bill Pay & Vendor Management API support with tests Comprehensive SDK update: adds enums, models, and client methods for PEX Bill Pay and Vendor Management APIs, including vendor and bill CRUD, approval, payment processing, and document handling. Includes a full end-to-end integration test and configuration support for secure, environment-specific testing. Also updates token model for richer authentication context. --- .../Enums/BillPaymentMethodType.cs | 8 + .../Enums/BillSortDirection.cs | 8 + .../Enums/BillSortField.cs | 10 + .../Enums/VendorStatus.cs | 10 + .../Enums/VendorStatusTrigger.cs | 12 + .../Enums/VendorStatusUpdateTrigger.cs | 15 + src/PexCard.Api.Client.Core/IPexApiClient.cs | 60 ++ .../Models/AddBillNoteRequestModel.cs | 9 + .../AddVendorBankAccountRequestModel.cs | 9 + .../Models/AddVendorCardRequestModel.cs | 7 + .../Models/ApproveBillRequestModel.cs | 7 + .../Models/BillApprovalDetailsModel.cs | 9 + .../Models/BillAttachmentModel.cs | 11 + .../Models/BillModel.cs | 29 + .../Models/BillNoteModel.cs | 15 + .../Models/BillNoteResponseModel.cs | 7 + .../Models/BillPaymentDetailsModel.cs | 11 + .../Models/BillPaymentItemModel.cs | 20 + .../Models/BillSearchItemModel.cs | 19 + .../Models/BillTagAllocationModel.cs | 10 + .../Models/BillTagValueModel.cs | 8 + .../Models/BillTagsModel.cs | 24 + .../Models/CreateBillRequestModel.cs | 12 + .../Models/CreateVendorRequestModel.cs | 18 + .../Models/GetBillPaymentsResponseModel.cs | 9 + .../Models/GetVendorsRequestModel.cs | 14 + .../Models/GetVendorsResponseModel.cs | 10 + .../Models/ProcessBillRequestModel.cs | 9 + .../Models/RejectBillRequestModel.cs | 7 + .../Models/SearchBillsRequestModel.cs | 26 + .../Models/SearchBillsResponseModel.cs | 10 + .../Models/SearchPageInfoModel.cs | 10 + .../Models/TokenDataModel.cs | 3 + .../Models/UpdateBillNoteRequestModel.cs | 9 + .../Models/UpdateBillTagsRequestModel.cs | 9 + .../UpdateVendorBankAccountRequestModel.cs | 9 + .../Models/UpdateVendorRequestModel.cs | 15 + .../Models/UpdateVendorStatusRequestModel.cs | 9 + .../UploadBillAttachmentResponseModel.cs | 9 + .../Models/VendorAddressModel.cs | 11 + .../Models/VendorBankAccountModel.cs | 12 + .../Models/VendorCardModel.cs | 12 + .../Models/VendorContactModel.cs | 10 + .../Models/VendorDocumentModel.cs | 13 + .../Models/VendorModel.cs | 28 + .../Models/VendorPagedItemModel.cs | 15 + src/PexCard.Api.Client/PexApiClient.cs | 494 ++++++++++++ .../VendorAndBillPaymentIntegrationTests.cs | 715 ++++++++++++++++++ .../PexCard.Api.Client.Core.Tests.csproj | 11 + .../appsettings.json | 6 + 50 files changed, 1833 insertions(+) create mode 100644 src/PexCard.Api.Client.Core/Enums/BillPaymentMethodType.cs create mode 100644 src/PexCard.Api.Client.Core/Enums/BillSortDirection.cs create mode 100644 src/PexCard.Api.Client.Core/Enums/BillSortField.cs create mode 100644 src/PexCard.Api.Client.Core/Enums/VendorStatus.cs create mode 100644 src/PexCard.Api.Client.Core/Enums/VendorStatusTrigger.cs create mode 100644 src/PexCard.Api.Client.Core/Enums/VendorStatusUpdateTrigger.cs create mode 100644 src/PexCard.Api.Client.Core/Models/AddBillNoteRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/AddVendorBankAccountRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/AddVendorCardRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/ApproveBillRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillApprovalDetailsModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillAttachmentModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillNoteModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillNoteResponseModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillPaymentDetailsModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillPaymentItemModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillSearchItemModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillTagAllocationModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillTagValueModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/BillTagsModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/CreateBillRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/CreateVendorRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/GetBillPaymentsResponseModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/GetVendorsRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/GetVendorsResponseModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/ProcessBillRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/RejectBillRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/SearchBillsRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/SearchBillsResponseModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/SearchPageInfoModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/UpdateBillNoteRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/UpdateBillTagsRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/UpdateVendorBankAccountRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/UpdateVendorRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/UpdateVendorStatusRequestModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/UploadBillAttachmentResponseModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorAddressModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorBankAccountModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorCardModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorContactModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorDocumentModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorModel.cs create mode 100644 src/PexCard.Api.Client.Core/Models/VendorPagedItemModel.cs create mode 100644 tests/PexCard.Api.Client.Core.Tests/Integration/VendorAndBillPaymentIntegrationTests.cs create mode 100644 tests/PexCard.Api.Client.Core.Tests/appsettings.json diff --git a/src/PexCard.Api.Client.Core/Enums/BillPaymentMethodType.cs b/src/PexCard.Api.Client.Core/Enums/BillPaymentMethodType.cs new file mode 100644 index 0000000..d885acf --- /dev/null +++ b/src/PexCard.Api.Client.Core/Enums/BillPaymentMethodType.cs @@ -0,0 +1,8 @@ +namespace PexCard.Api.Client.Core.Enums +{ + public enum BillPaymentMethodType + { + VendorCard, + ACH + } +} diff --git a/src/PexCard.Api.Client.Core/Enums/BillSortDirection.cs b/src/PexCard.Api.Client.Core/Enums/BillSortDirection.cs new file mode 100644 index 0000000..99fc815 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Enums/BillSortDirection.cs @@ -0,0 +1,8 @@ +namespace PexCard.Api.Client.Core.Enums +{ + public enum BillSortDirection + { + Ascending, + Descending + } +} diff --git a/src/PexCard.Api.Client.Core/Enums/BillSortField.cs b/src/PexCard.Api.Client.Core/Enums/BillSortField.cs new file mode 100644 index 0000000..0650dc6 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Enums/BillSortField.cs @@ -0,0 +1,10 @@ +namespace PexCard.Api.Client.Core.Enums +{ + public enum BillSortField + { + Id, + Amount, + Created, + DueDate + } +} diff --git a/src/PexCard.Api.Client.Core/Enums/VendorStatus.cs b/src/PexCard.Api.Client.Core/Enums/VendorStatus.cs new file mode 100644 index 0000000..a23eff6 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Enums/VendorStatus.cs @@ -0,0 +1,10 @@ +namespace PexCard.Api.Client.Core.Enums +{ + public enum VendorStatus + { + Draft = 10, + Pending = 20, + Onboarded = 30, + Closed = 40 + } +} diff --git a/src/PexCard.Api.Client.Core/Enums/VendorStatusTrigger.cs b/src/PexCard.Api.Client.Core/Enums/VendorStatusTrigger.cs new file mode 100644 index 0000000..eed87db --- /dev/null +++ b/src/PexCard.Api.Client.Core/Enums/VendorStatusTrigger.cs @@ -0,0 +1,12 @@ +namespace PexCard.Api.Client.Core.Enums +{ + public enum VendorStatusTrigger + { + New = 10, + Submitted = 20, + Active = 30, + Inactive = 35, + Rejected = 40, + Offboarded = 45 + } +} diff --git a/src/PexCard.Api.Client.Core/Enums/VendorStatusUpdateTrigger.cs b/src/PexCard.Api.Client.Core/Enums/VendorStatusUpdateTrigger.cs new file mode 100644 index 0000000..b32f537 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Enums/VendorStatusUpdateTrigger.cs @@ -0,0 +1,15 @@ +namespace PexCard.Api.Client.Core.Enums +{ + /// + /// Status triggers allowed for vendor status update operations. + /// This is a subset of VendorStatusTrigger that excludes triggers + /// that require dedicated endpoints (Rejected) or are only used + /// during vendor creation (New). + /// + public enum VendorStatusUpdateTrigger + { + Active = 30, + Inactive = 35, + Offboarded = 45 + } +} diff --git a/src/PexCard.Api.Client.Core/IPexApiClient.cs b/src/PexCard.Api.Client.Core/IPexApiClient.cs index 51c99fb..f45aa68 100644 --- a/src/PexCard.Api.Client.Core/IPexApiClient.cs +++ b/src/PexCard.Api.Client.Core/IPexApiClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using PexCard.Api.Client.Core.Enums; @@ -135,6 +136,65 @@ public interface IPexApiClient Task SendVendorCardData(string externalToken, VendorCardDataModel data, CancellationToken cancelToken = default); + // Vendor Management + Task GetVendors(string externalToken, GetVendorsRequestModel request = null, CancellationToken cancelToken = default); + + Task GetVendor(string externalToken, int vendorId, CancellationToken cancelToken = default); + + Task CreateVendor(string externalToken, CreateVendorRequestModel request, CancellationToken cancelToken = default); + + Task UpdateVendor(string externalToken, int vendorId, UpdateVendorRequestModel request, CancellationToken cancelToken = default); + + Task UpdateVendorStatus(string externalToken, int vendorId, UpdateVendorStatusRequestModel request, CancellationToken cancelToken = default); + + Task ApproveVendor(string externalToken, int vendorId, CancellationToken cancelToken = default); + + Task RejectVendor(string externalToken, int vendorId, CancellationToken cancelToken = default); + + // Vendor Documents + Task AddVendorDocument(string externalToken, int vendorId, string fileName, Stream fileContent, CancellationToken cancelToken = default); + + Task DeleteVendorDocument(string externalToken, int vendorId, int documentId, CancellationToken cancelToken = default); + + // Vendor Cards (linked cardholder accounts) + Task AddVendorCard(string externalToken, int vendorId, AddVendorCardRequestModel request, CancellationToken cancelToken = default); + + Task UpdateVendorCard(string externalToken, int vendorId, int cardholderAcctId, CancellationToken cancelToken = default); + + // Vendor Bank Accounts + Task AddVendorBankAccount(string externalToken, int vendorId, AddVendorBankAccountRequestModel request, CancellationToken cancelToken = default); + + Task UpdateVendorBankAccount(string externalToken, int vendorId, long bankAccountId, UpdateVendorBankAccountRequestModel request, CancellationToken cancelToken = default); + + // Bill Payments + Task GetBill(string externalToken, int billId, CancellationToken cancelToken = default); + + Task GetBillPayments(string externalToken, int billId, CancellationToken cancelToken = default); + + Task CreateBill(string externalToken, CreateBillRequestModel request, CancellationToken cancelToken = default); + + Task SearchBills(string externalToken, SearchBillsRequestModel request = null, CancellationToken cancelToken = default); + + Task UpdateBillTags(string externalToken, int billId, UpdateBillTagsRequestModel request, CancellationToken cancelToken = default); + + Task DeleteBillTags(string externalToken, int billId, CancellationToken cancelToken = default); + + Task AddBillNote(string externalToken, int billId, AddBillNoteRequestModel request, CancellationToken cancelToken = default); + + Task UpdateBillNote(string externalToken, int billId, long noteId, UpdateBillNoteRequestModel request, CancellationToken cancelToken = default); + + Task DeleteBillNote(string externalToken, int billId, long noteId, CancellationToken cancelToken = default); + + Task AddBillAttachment(string externalToken, int billId, string fileName, Stream fileContent, CancellationToken cancelToken = default); + + Task SubmitBill(string externalToken, int billId, CancellationToken cancelToken = default); + + Task ApproveBill(string externalToken, int billId, ApproveBillRequestModel request = null, CancellationToken cancelToken = default); + + Task RejectBill(string externalToken, int billId, RejectBillRequestModel request = null, CancellationToken cancelToken = default); + + Task ProcessBill(string externalToken, int billId, ProcessBillRequestModel request = null, CancellationToken cancelToken = default); + Task GetSpendingRulesets(string externalToken, CancellationToken cancelToken = default); Task GetSpendingRuleset(string externalToken, int rulesetId, CancellationToken cancelToken = default); diff --git a/src/PexCard.Api.Client.Core/Models/AddBillNoteRequestModel.cs b/src/PexCard.Api.Client.Core/Models/AddBillNoteRequestModel.cs new file mode 100644 index 0000000..d65e409 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/AddBillNoteRequestModel.cs @@ -0,0 +1,9 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class AddBillNoteRequestModel + { + public string Note { get; set; } + public bool VisibleToCardholder { get; set; } + public bool SystemGenerated { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/AddVendorBankAccountRequestModel.cs b/src/PexCard.Api.Client.Core/Models/AddVendorBankAccountRequestModel.cs new file mode 100644 index 0000000..5cd47bb --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/AddVendorBankAccountRequestModel.cs @@ -0,0 +1,9 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class AddVendorBankAccountRequestModel + { + public string BankName { get; set; } + public string BankAccountNumber { get; set; } + public string BankRoutingNumber { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/AddVendorCardRequestModel.cs b/src/PexCard.Api.Client.Core/Models/AddVendorCardRequestModel.cs new file mode 100644 index 0000000..5037286 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/AddVendorCardRequestModel.cs @@ -0,0 +1,7 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class AddVendorCardRequestModel + { + public int CardholderAcctId { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/ApproveBillRequestModel.cs b/src/PexCard.Api.Client.Core/Models/ApproveBillRequestModel.cs new file mode 100644 index 0000000..ab5f6e4 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/ApproveBillRequestModel.cs @@ -0,0 +1,7 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class ApproveBillRequestModel + { + public string Reason { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillApprovalDetailsModel.cs b/src/PexCard.Api.Client.Core/Models/BillApprovalDetailsModel.cs new file mode 100644 index 0000000..db0b166 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillApprovalDetailsModel.cs @@ -0,0 +1,9 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class BillApprovalDetailsModel + { + public long? ApprovalId { get; set; } + public string ApprovalStatus { get; set; } + public string ApprovalWorkflowType { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillAttachmentModel.cs b/src/PexCard.Api.Client.Core/Models/BillAttachmentModel.cs new file mode 100644 index 0000000..40dd62d --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillAttachmentModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillAttachmentModel + { + public string Id { get; set; } + public string FileName { get; set; } + public DateTimeOffset UploadedDate { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillModel.cs b/src/PexCard.Api.Client.Core/Models/BillModel.cs new file mode 100644 index 0000000..3b3bbbb --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillModel + { + public int BillId { get; set; } + public string MerchantName { get; set; } + public decimal Amount { get; set; } + public string PaymentRequestStatus { get; set; } + public string PaymentRequestStatusTrigger { get; set; } + public string PaymentRequestType { get; set; } + public string PayeeFundsDestinationType { get; set; } + public DateTimeOffset PurchaseDate { get; set; } + public DateTimeOffset? PayoutDate { get; set; } + public long? MetadataId { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public bool HasPaymentError { get; set; } + public long? PayeePexId { get; set; } + public BillPaymentDetailsModel BillPayment { get; set; } + public BillApprovalDetailsModel ApprovalDetails { get; set; } + public bool IsUserApprovalRestricted { get; set; } + public bool IsUserInApprovalRoute { get; set; } + public List Attachments { get; set; } + public BillTagsModel Tags { get; set; } + public List Notes { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillNoteModel.cs b/src/PexCard.Api.Client.Core/Models/BillNoteModel.cs new file mode 100644 index 0000000..49436e5 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillNoteModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillNoteModel + { + public long Id { get; set; } + public string Note { get; set; } + public bool VisibleToCardholder { get; set; } + public bool SystemGenerated { get; set; } + public long? CreatedByUserId { get; set; } + public string CreatedByUser { get; set; } + public DateTime CreatedDateTime { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillNoteResponseModel.cs b/src/PexCard.Api.Client.Core/Models/BillNoteResponseModel.cs new file mode 100644 index 0000000..5c2421b --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillNoteResponseModel.cs @@ -0,0 +1,7 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class BillNoteResponseModel + { + public long NoteId { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillPaymentDetailsModel.cs b/src/PexCard.Api.Client.Core/Models/BillPaymentDetailsModel.cs new file mode 100644 index 0000000..8276472 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillPaymentDetailsModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillPaymentDetailsModel + { + public DateTimeOffset BillDate { get; set; } + public DateTimeOffset? DueDate { get; set; } + public string BillRefNo { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillPaymentItemModel.cs b/src/PexCard.Api.Client.Core/Models/BillPaymentItemModel.cs new file mode 100644 index 0000000..5376867 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillPaymentItemModel.cs @@ -0,0 +1,20 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillPaymentItemModel + { + public int PaymentId { get; set; } + public string PaymentStatus { get; set; } + public string PaymentStatusTrigger { get; set; } + public int? PaymentTransferId { get; set; } + public decimal Amount { get; set; } + public long? PayeeBankAccountId { get; set; } + public long? PayeeUserId { get; set; } + public DateTimeOffset? PayoutDate { get; set; } + public DateTimeOffset? OutboundAchCreationDate { get; set; } + public DateTimeOffset? ExpectedPaymentDate { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset? Updated { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillSearchItemModel.cs b/src/PexCard.Api.Client.Core/Models/BillSearchItemModel.cs new file mode 100644 index 0000000..d369bda --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillSearchItemModel.cs @@ -0,0 +1,19 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillSearchItemModel + { + public int BillId { get; set; } + public string BillRefNo { get; set; } + public decimal Amount { get; set; } + public DateTimeOffset Created { get; set; } + public long CreatedByUserId { get; set; } + public string CreatedByUser { get; set; } + public long PayeePexId { get; set; } + public string PayeeName { get; set; } + public DateTimeOffset? DueDate { get; set; } + public string PaymentRequestStatus { get; set; } + public string PaymentRequestStatusTrigger { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillTagAllocationModel.cs b/src/PexCard.Api.Client.Core/Models/BillTagAllocationModel.cs new file mode 100644 index 0000000..df4f926 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillTagAllocationModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillTagAllocationModel + { + public decimal Amount { get; set; } + public List Tags { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillTagValueModel.cs b/src/PexCard.Api.Client.Core/Models/BillTagValueModel.cs new file mode 100644 index 0000000..2e63c5b --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillTagValueModel.cs @@ -0,0 +1,8 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class BillTagValueModel + { + public string TagId { get; set; } + public object Value { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/BillTagsModel.cs b/src/PexCard.Api.Client.Core/Models/BillTagsModel.cs new file mode 100644 index 0000000..a896791 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/BillTagsModel.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class BillTagsModel + { + public bool IsSplit { get; set; } + public List Allocations { get; set; } + } + + public class BillTagAllocationResponseModel + { + public decimal Amount { get; set; } + public List Tags { get; set; } + } + + public class BillTagValueResponseModel + { + public string TagId { get; set; } + public string TagName { get; set; } + public object TagOptionValue { get; set; } + public string TagOptionName { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/CreateBillRequestModel.cs b/src/PexCard.Api.Client.Core/Models/CreateBillRequestModel.cs new file mode 100644 index 0000000..6fca0b4 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/CreateBillRequestModel.cs @@ -0,0 +1,12 @@ +using PexCard.Api.Client.Core.Enums; + +namespace PexCard.Api.Client.Core.Models +{ + public class CreateBillRequestModel + { + public int VendorId { get; set; } + public decimal Amount { get; set; } + public BillPaymentMethodType? PaymentMethod { get; set; } + public BillPaymentDetailsModel BillPayment { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/CreateVendorRequestModel.cs b/src/PexCard.Api.Client.Core/Models/CreateVendorRequestModel.cs new file mode 100644 index 0000000..46b6974 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/CreateVendorRequestModel.cs @@ -0,0 +1,18 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class CreateVendorRequestModel + { + public string VendorName { get; set; } + public string EmailForRemittance { get; set; } + public string WebSite { get; set; } + public string CustomId { get; set; } + public string TaxId { get; set; } + public string Note { get; set; } + public VendorAddressModel VendorAddress { get; set; } + public VendorContactModel VendorContact { get; set; } + public long? MerchantId { get; set; } + public bool SendNotification { get; set; } + public bool VendorCardPaymentEnabled { get; set; } + public bool AchPaymentEnabled { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/GetBillPaymentsResponseModel.cs b/src/PexCard.Api.Client.Core/Models/GetBillPaymentsResponseModel.cs new file mode 100644 index 0000000..47c0927 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/GetBillPaymentsResponseModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class GetBillPaymentsResponseModel + { + public List Payments { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/GetVendorsRequestModel.cs b/src/PexCard.Api.Client.Core/Models/GetVendorsRequestModel.cs new file mode 100644 index 0000000..6ceb49a --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/GetVendorsRequestModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using PexCard.Api.Client.Core.Enums; + +namespace PexCard.Api.Client.Core.Models +{ + public class GetVendorsRequestModel + { + public int? CardholderAcctId { get; set; } + public List VendorStatuses { get; set; } + public List VendorStatusTriggers { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/src/PexCard.Api.Client.Core/Models/GetVendorsResponseModel.cs b/src/PexCard.Api.Client.Core/Models/GetVendorsResponseModel.cs new file mode 100644 index 0000000..51e4407 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/GetVendorsResponseModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class GetVendorsResponseModel + { + public List Vendors { get; set; } + public long TotalCount { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/ProcessBillRequestModel.cs b/src/PexCard.Api.Client.Core/Models/ProcessBillRequestModel.cs new file mode 100644 index 0000000..6a91841 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/ProcessBillRequestModel.cs @@ -0,0 +1,9 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class ProcessBillRequestModel + { + public DateTimeOffset? ProcessingDate { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/RejectBillRequestModel.cs b/src/PexCard.Api.Client.Core/Models/RejectBillRequestModel.cs new file mode 100644 index 0000000..233623a --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/RejectBillRequestModel.cs @@ -0,0 +1,7 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class RejectBillRequestModel + { + public string Note { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/SearchBillsRequestModel.cs b/src/PexCard.Api.Client.Core/Models/SearchBillsRequestModel.cs new file mode 100644 index 0000000..9173bf1 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/SearchBillsRequestModel.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using PexCard.Api.Client.Core.Enums; + +namespace PexCard.Api.Client.Core.Models +{ + public class SearchBillsRequestModel + { + public DateTimeOffset? CreatedDateFrom { get; set; } + public DateTimeOffset? CreatedDateTo { get; set; } + public DateTimeOffset? DueDateFrom { get; set; } + public DateTimeOffset? DueDateTo { get; set; } + public long? CreatedByUserId { get; set; } + public int? VendorId { get; set; } + public decimal? AmountFrom { get; set; } + public decimal? AmountTo { get; set; } + public List PaymentRequestStatuses { get; set; } + public List PaymentRequestStatusTriggers { get; set; } + public bool IsOwner { get; set; } + public bool IsPendingReview { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 25; + public BillSortDirection? SortDirection { get; set; } + public BillSortField? SortByField { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/SearchBillsResponseModel.cs b/src/PexCard.Api.Client.Core/Models/SearchBillsResponseModel.cs new file mode 100644 index 0000000..7e32905 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/SearchBillsResponseModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class SearchBillsResponseModel + { + public List Items { get; set; } + public SearchPageInfoModel PageInfo { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/SearchPageInfoModel.cs b/src/PexCard.Api.Client.Core/Models/SearchPageInfoModel.cs new file mode 100644 index 0000000..4b0eea3 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/SearchPageInfoModel.cs @@ -0,0 +1,10 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class SearchPageInfoModel + { + public int TotalCount { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalPages { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/TokenDataModel.cs b/src/PexCard.Api.Client.Core/Models/TokenDataModel.cs index 78a4128..a92ddb0 100644 --- a/src/PexCard.Api.Client.Core/Models/TokenDataModel.cs +++ b/src/PexCard.Api.Client.Core/Models/TokenDataModel.cs @@ -17,6 +17,9 @@ public class TokenDataModel /// UserId associated with the token public int? CardholderAccountId { get; set; } + /// PlatformAccountId associated with the token (for admin users) + public int? PlatformAccountId { get; set; } + /// UserType associated with the token public UserType UserType { get; set; } diff --git a/src/PexCard.Api.Client.Core/Models/UpdateBillNoteRequestModel.cs b/src/PexCard.Api.Client.Core/Models/UpdateBillNoteRequestModel.cs new file mode 100644 index 0000000..ac9165d --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/UpdateBillNoteRequestModel.cs @@ -0,0 +1,9 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class UpdateBillNoteRequestModel + { + public string Note { get; set; } + public bool VisibleToCardholder { get; set; } + public bool SystemGenerated { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/UpdateBillTagsRequestModel.cs b/src/PexCard.Api.Client.Core/Models/UpdateBillTagsRequestModel.cs new file mode 100644 index 0000000..b651e90 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/UpdateBillTagsRequestModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class UpdateBillTagsRequestModel + { + public List Tags { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/UpdateVendorBankAccountRequestModel.cs b/src/PexCard.Api.Client.Core/Models/UpdateVendorBankAccountRequestModel.cs new file mode 100644 index 0000000..2744beb --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/UpdateVendorBankAccountRequestModel.cs @@ -0,0 +1,9 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class UpdateVendorBankAccountRequestModel + { + public string BankName { get; set; } + public string BankAccountNumber { get; set; } + public string BankRoutingNumber { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/UpdateVendorRequestModel.cs b/src/PexCard.Api.Client.Core/Models/UpdateVendorRequestModel.cs new file mode 100644 index 0000000..9c4a18c --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/UpdateVendorRequestModel.cs @@ -0,0 +1,15 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class UpdateVendorRequestModel + { + public string VendorName { get; set; } + public string EmailForRemittance { get; set; } + public string WebSite { get; set; } + public string CustomId { get; set; } + public string TaxId { get; set; } + public string Note { get; set; } + public VendorAddressModel VendorAddress { get; set; } + public VendorContactModel VendorContact { get; set; } + public bool? VendorCardPaymentEnabled { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/UpdateVendorStatusRequestModel.cs b/src/PexCard.Api.Client.Core/Models/UpdateVendorStatusRequestModel.cs new file mode 100644 index 0000000..5a486e8 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/UpdateVendorStatusRequestModel.cs @@ -0,0 +1,9 @@ +using PexCard.Api.Client.Core.Enums; + +namespace PexCard.Api.Client.Core.Models +{ + public class UpdateVendorStatusRequestModel + { + public VendorStatusUpdateTrigger NewStatus { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/UploadBillAttachmentResponseModel.cs b/src/PexCard.Api.Client.Core/Models/UploadBillAttachmentResponseModel.cs new file mode 100644 index 0000000..bfc09d0 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/UploadBillAttachmentResponseModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace PexCard.Api.Client.Core.Models +{ + public class UploadBillAttachmentResponseModel + { + public List Attachments { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorAddressModel.cs b/src/PexCard.Api.Client.Core/Models/VendorAddressModel.cs new file mode 100644 index 0000000..d742374 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorAddressModel.cs @@ -0,0 +1,11 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class VendorAddressModel + { + public string AddressLine1 { get; set; } + public string AddressLine2 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorBankAccountModel.cs b/src/PexCard.Api.Client.Core/Models/VendorBankAccountModel.cs new file mode 100644 index 0000000..c56c484 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorBankAccountModel.cs @@ -0,0 +1,12 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class VendorBankAccountModel + { + public int VendorId { get; set; } + public long BankAccountId { get; set; } + public bool IsActive { get; set; } + public DateTimeOffset DateCreated { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorCardModel.cs b/src/PexCard.Api.Client.Core/Models/VendorCardModel.cs new file mode 100644 index 0000000..b3bff05 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorCardModel.cs @@ -0,0 +1,12 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class VendorCardModel + { + public int CardholderAcctId { get; set; } + public string CardNumber4Digits { get; set; } + public string CardStatus { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public bool IsDefault { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorContactModel.cs b/src/PexCard.Api.Client.Core/Models/VendorContactModel.cs new file mode 100644 index 0000000..5f2b058 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorContactModel.cs @@ -0,0 +1,10 @@ +namespace PexCard.Api.Client.Core.Models +{ + public class VendorContactModel + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string PhoneNumber { get; set; } + public string Email { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorDocumentModel.cs b/src/PexCard.Api.Client.Core/Models/VendorDocumentModel.cs new file mode 100644 index 0000000..ab1ce3d --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorDocumentModel.cs @@ -0,0 +1,13 @@ +using System; + +namespace PexCard.Api.Client.Core.Models +{ + public class VendorDocumentModel + { + public int DocumentId { get; set; } + public long UploadedByUserId { get; set; } + public DateTimeOffset DateCreated { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorModel.cs b/src/PexCard.Api.Client.Core/Models/VendorModel.cs new file mode 100644 index 0000000..b6ac6e3 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorModel.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using PexCard.Api.Client.Core.Enums; + +namespace PexCard.Api.Client.Core.Models +{ + public class VendorModel + { + public int VendorId { get; set; } + public string VendorName { get; set; } + public string EmailForRemittance { get; set; } + public string Website { get; set; } + public string LogoUrl { get; set; } + public VendorStatus VendorStatus { get; set; } + public VendorStatusTrigger VendorStatusTrigger { get; set; } + public bool? VendorCardPaymentEnabled { get; set; } + public bool? AchPaymentEnabled { get; set; } + public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public VendorAddressModel VendorAddress { get; set; } + public VendorContactModel VendorContact { get; set; } + public string CustomId { get; set; } + public string Note { get; set; } + public List Documents { get; set; } + public List VendorCards { get; set; } + public List BankAccounts { get; set; } + } +} diff --git a/src/PexCard.Api.Client.Core/Models/VendorPagedItemModel.cs b/src/PexCard.Api.Client.Core/Models/VendorPagedItemModel.cs new file mode 100644 index 0000000..ae248b6 --- /dev/null +++ b/src/PexCard.Api.Client.Core/Models/VendorPagedItemModel.cs @@ -0,0 +1,15 @@ +using PexCard.Api.Client.Core.Enums; + +namespace PexCard.Api.Client.Core.Models +{ + public class VendorPagedItemModel + { + public int VendorId { get; set; } + public string VendorName { get; set; } + public string IconUrl { get; set; } + public VendorStatus VendorStatus { get; set; } + public VendorStatusTrigger VendorStatusTrigger { get; set; } + public long CreatedByUserId { get; set; } + public string Owner { get; set; } + } +} diff --git a/src/PexCard.Api.Client/PexApiClient.cs b/src/PexCard.Api.Client/PexApiClient.cs index d5ae37c..b94f02c 100644 --- a/src/PexCard.Api.Client/PexApiClient.cs +++ b/src/PexCard.Api.Client/PexApiClient.cs @@ -9,9 +9,11 @@ using PexCard.Api.Client.Models; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using System.Web; @@ -1011,6 +1013,498 @@ public async Task SendVendorCardData(string externalToken, VendorCardDataModel d await HandleHttpResponseMessage(response); } + #region Vendor Management + + public async Task GetVendors(string externalToken, GetVendorsRequestModel request = null, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, "V4/Vendor")); + + request = request ?? new GetVendorsRequestModel(); + var requestUriQueryParams = HttpUtility.ParseQueryString(requestUriBuilder.Query); + + if (request.CardholderAcctId.HasValue) + { + requestUriQueryParams.Add("cardholderAcctId", request.CardholderAcctId.Value.ToString()); + } + + if (request.VendorStatuses != null) + { + foreach (var status in request.VendorStatuses) + { + requestUriQueryParams.Add("vendorStatuses", status.ToString()); + } + } + + if (request.VendorStatusTriggers != null) + { + foreach (var trigger in request.VendorStatusTriggers) + { + requestUriQueryParams.Add("vendorStatusTriggers", trigger.ToString()); + } + } + + requestUriQueryParams.Add("pageIndex", request.PageIndex.ToString()); + requestUriQueryParams.Add("pageSize", request.PageSize.ToString()); + requestUriBuilder.Query = requestUriQueryParams.ToString(); + + var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task GetVendor(string externalToken, int vendorId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}")); + + var request = new HttpRequestMessage(HttpMethod.Get, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task CreateVendor(string externalToken, CreateVendorRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, "V4/Vendor")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task UpdateVendor(string externalToken, int vendorId, UpdateVendorRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task UpdateVendorStatus(string externalToken, int vendorId, UpdateVendorStatusRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Status")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task ApproveVendor(string externalToken, int vendorId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Approve")); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task RejectVendor(string externalToken, int vendorId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Reject")); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + #endregion + + #region Vendor Documents + + public async Task AddVendorDocument(string externalToken, int vendorId, string fileName, Stream fileContent, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Document")); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAuthorizationTokenHeader(externalToken); + + var multipartContent = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileContent); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + multipartContent.Add(streamContent, "file", fileName); + request.Content = multipartContent; + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task DeleteVendorDocument(string externalToken, int vendorId, int documentId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Document/{documentId}")); + + var request = new HttpRequestMessage(HttpMethod.Delete, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + #endregion + + #region Vendor Cards + + public async Task AddVendorCard(string externalToken, int vendorId, AddVendorCardRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Card")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task UpdateVendorCard(string externalToken, int vendorId, int cardholderAcctId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/Card/{cardholderAcctId}")); + + var request = new HttpRequestMessage(HttpMethod.Put, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + #endregion + + #region Vendor Bank Accounts + + public async Task AddVendorBankAccount(string externalToken, int vendorId, AddVendorBankAccountRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/BankAccount")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task UpdateVendorBankAccount(string externalToken, int vendorId, long bankAccountId, UpdateVendorBankAccountRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Vendor/{vendorId}/BankAccount/{bankAccountId}")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + #endregion + + #region Bill Payments + + public async Task GetBill(string externalToken, int billId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}")); + + var request = new HttpRequestMessage(HttpMethod.Get, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task GetBillPayments(string externalToken, int billId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Payments")); + + var request = new HttpRequestMessage(HttpMethod.Get, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task CreateBill(string externalToken, CreateBillRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, "V4/Bill")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task SearchBills(string externalToken, SearchBillsRequestModel request = null, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, "V4/Bill")); + + request = request ?? new SearchBillsRequestModel(); + var requestUriQueryParams = HttpUtility.ParseQueryString(requestUriBuilder.Query); + + if (request.CreatedDateFrom.HasValue) + requestUriQueryParams.Add("CreatedDateFrom", request.CreatedDateFrom.Value.ToString("o")); + if (request.CreatedDateTo.HasValue) + requestUriQueryParams.Add("CreatedDateTo", request.CreatedDateTo.Value.ToString("o")); + if (request.DueDateFrom.HasValue) + requestUriQueryParams.Add("DueDateFrom", request.DueDateFrom.Value.ToString("o")); + if (request.DueDateTo.HasValue) + requestUriQueryParams.Add("DueDateTo", request.DueDateTo.Value.ToString("o")); + if (request.CreatedByUserId.HasValue) + requestUriQueryParams.Add("CreatedByUserId", request.CreatedByUserId.Value.ToString()); + if (request.VendorId.HasValue) + requestUriQueryParams.Add("VendorId", request.VendorId.Value.ToString()); + if (request.AmountFrom.HasValue) + requestUriQueryParams.Add("AmountFrom", request.AmountFrom.Value.ToString()); + if (request.AmountTo.HasValue) + requestUriQueryParams.Add("AmountTo", request.AmountTo.Value.ToString()); + if (request.PaymentRequestStatuses != null) + { + foreach (var status in request.PaymentRequestStatuses) + requestUriQueryParams.Add("PaymentRequestStatuses", status); + } + if (request.PaymentRequestStatusTriggers != null) + { + foreach (var trigger in request.PaymentRequestStatusTriggers) + requestUriQueryParams.Add("PaymentRequestStatusTriggers", trigger); + } + if (request.IsOwner) + requestUriQueryParams.Add("IsOwner", "true"); + if (request.IsPendingReview) + requestUriQueryParams.Add("IsPendingReview", "true"); + requestUriQueryParams.Add("Page", request.Page.ToString()); + requestUriQueryParams.Add("PageSize", request.PageSize.ToString()); + if (request.SortDirection.HasValue) + requestUriQueryParams.Add("SortDirection", request.SortDirection.Value.ToString()); + if (request.SortByField.HasValue) + requestUriQueryParams.Add("SortByField", request.SortByField.Value.ToString()); + requestUriBuilder.Query = requestUriQueryParams.ToString(); + + var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task UpdateBillTags(string externalToken, int billId, UpdateBillTagsRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Tags")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + await HandleHttpResponseMessage(response); + } + + public async Task DeleteBillTags(string externalToken, int billId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Tags")); + + var request = new HttpRequestMessage(HttpMethod.Delete, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + await HandleHttpResponseMessage(response); + } + + public async Task AddBillNote(string externalToken, int billId, AddBillNoteRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Note")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task UpdateBillNote(string externalToken, int billId, long noteId, UpdateBillNoteRequestModel request, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Note/{noteId}")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + await HandleHttpResponseMessage(response); + } + + public async Task DeleteBillNote(string externalToken, int billId, long noteId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Note/{noteId}")); + + var request = new HttpRequestMessage(HttpMethod.Delete, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + await HandleHttpResponseMessage(response); + } + + public async Task AddBillAttachment(string externalToken, int billId, string fileName, Stream fileContent, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Attachment")); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAuthorizationTokenHeader(externalToken); + + var multipartContent = new MultipartFormDataContent(); + var streamContent = new StreamContent(fileContent); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + multipartContent.Add(streamContent, "file", fileName); + request.Content = multipartContent; + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task SubmitBill(string externalToken, int billId, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Submit")); + + var request = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + request.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + request.SetPexAcceptJsonHeader(); + request.SetPexAuthorizationTokenHeader(externalToken); + + var response = await _httpClient.SendAsync(request, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task ApproveBill(string externalToken, int billId, ApproveBillRequestModel request = null, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Approve")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + if (request != null) + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task RejectBill(string externalToken, int billId, RejectBillRequestModel request = null, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Reject")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + if (request != null) + httpRequest.SetPexJsonContent(request); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + public async Task ProcessBill(string externalToken, int billId, ProcessBillRequestModel request = null, CancellationToken cancelToken = default) + { + var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/Bill/{billId}/Process")); + + var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUriBuilder.Uri); + httpRequest.SetPexCorrelationIdHeader(_correlationIdResolver.GetValue()); + httpRequest.SetPexAcceptJsonHeader(); + httpRequest.SetPexAuthorizationTokenHeader(externalToken); + httpRequest.SetPexJsonContent(request ?? new ProcessBillRequestModel()); + + var response = await _httpClient.SendAsync(httpRequest, cancelToken); + + return await HandleHttpResponseMessage(response); + } + + #endregion + public async Task GetSpendingRulesets(string externalToken, CancellationToken cancelToken = default) { var requestUriBuilder = new UriBuilder(new Uri(BaseUri, $"V4/SpendingRuleset")); diff --git a/tests/PexCard.Api.Client.Core.Tests/Integration/VendorAndBillPaymentIntegrationTests.cs b/tests/PexCard.Api.Client.Core.Tests/Integration/VendorAndBillPaymentIntegrationTests.cs new file mode 100644 index 0000000..1fbb992 --- /dev/null +++ b/tests/PexCard.Api.Client.Core.Tests/Integration/VendorAndBillPaymentIntegrationTests.cs @@ -0,0 +1,715 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using PexCard.Api.Client; +using PexCard.Api.Client.Core; +using PexCard.Api.Client.Core.Enums; +using PexCard.Api.Client.Core.Models; +using Xunit; +using Xunit.Abstractions; + +namespace PexCard.Api.Client.Core.Tests.Integration +{ + /// + /// End-to-end integration tests and documentation for PEX Bill Pay workflows. + /// + /// This test serves as living documentation demonstrating the complete flow + /// for paying vendors via ACH using the PEX Bill Pay API. + /// + /// Prerequisites: + /// - Configure appsettings.local.json with PexApi:Token set to a valid external token + /// - Test business must have Bill Pay and Vendor Management enabled + /// - Business must have ACH payments enabled + /// + /// Configuration: + /// - appsettings.json contains defaults (committed to repo) + /// - appsettings.local.json contains your credentials (not committed, add to .gitignore) + /// + [Trait("Category", "Integration")] + public class VendorAndBillPaymentIntegrationTests + { + private readonly ITestOutputHelper _output; + private readonly IPexApiClient _pexApiClient; + private readonly string _apiToken; + + public VendorAndBillPaymentIntegrationTests(ITestOutputHelper output) + { + _output = output; + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false) + .AddJsonFile("appsettings.local.json", optional: true) + .Build(); + + var apiBaseUrl = configuration["PexApi:BaseUrl"] ?? "https://coreapi.pexcard.com"; + _apiToken = configuration["PexApi:Token"]; + + if (string.IsNullOrEmpty(_apiToken) || _apiToken == "YOUR_TOKEN_HERE") + { + _output.WriteLine("WARNING: PexApi:Token not configured in appsettings.local.json. Integration tests will be skipped."); + _apiToken = null; + } + + var httpClient = new HttpClient { BaseAddress = new Uri(apiBaseUrl) }; + _pexApiClient = new PexApiClient(httpClient); + } + + /// + /// Complete Bill Pay workflow demonstration. + /// This test shows how to pay a vendor via ACH from start to finish. + /// + [Fact] + public async Task EndToEnd_VendorAndBillPayment_CompletesFullWorkflow() + { + // Skip if no token configured + if (string.IsNullOrEmpty(_apiToken)) + { + _output.WriteLine("Skipping test - PexApi:Token not configured in appsettings.local.json"); + return; + } + + var testAmount = 100.00m + (decimal)new Random().Next(1, 99) / 100; + var vendorName = $"SDK-Test-Vendor-{DateTime.Now:yyyyMMdd-HHmmss}"; + var billRefNo = $"SDK-BILL-{DateTime.Now:yyyyMMdd-HHmmss}-{new Random().Next(1000, 9999)}"; + + PrintHeader(); + PrintWorkflowOverview(); + await PrintBusinessContext(); + + // ═══════════════════════════════════════════════════════════════════════════════════════ + // PHASE 1: VENDOR SETUP + // Before you can pay a vendor, you must create and onboard them in the system. + // ═══════════════════════════════════════════════════════════════════════════════════════ + + PrintPhaseHeader("PHASE 1: VENDOR SETUP", "Before paying a vendor, they must be created and onboarded"); + + // Step 1: Create Vendor + var vendor = await Step1_CreateVendor(vendorName); + + // Step 2: Add Bank Account + vendor = await Step2_AddBankAccount(vendor.VendorId); + + // Step 3: Approve Vendor (if required by business policy) + vendor = await Step3_ApproveVendor(vendor); + + // ═══════════════════════════════════════════════════════════════════════════════════════ + // PHASE 2: BILL CREATION + // Create a bill (payment request) for the vendor. + // ═══════════════════════════════════════════════════════════════════════════════════════ + + PrintPhaseHeader("PHASE 2: BILL CREATION", "Create a payment request for the vendor"); + + // Step 4: Create Bill + var bill = await Step4_CreateBill(vendor.VendorId, testAmount, billRefNo); + + // ═══════════════════════════════════════════════════════════════════════════════════════ + // PHASE 3: APPROVAL WORKFLOW + // Bills go through an approval process before payment. + // The workflow depends on your business configuration. + // ═══════════════════════════════════════════════════════════════════════════════════════ + + PrintPhaseHeader("PHASE 3: APPROVAL WORKFLOW", "Bills must be approved before payment processing"); + + // Step 5: Submit Bill for Approval + bill = await Step5_SubmitBill(bill.BillId); + + // Step 6: Approve Bill + bill = await Step6_ApproveBill(bill); + + // ═══════════════════════════════════════════════════════════════════════════════════════ + // PHASE 4: PAYMENT PROCESSING + // Process the approved bill to initiate the ACH transfer. + // ═══════════════════════════════════════════════════════════════════════════════════════ + + PrintPhaseHeader("PHASE 4: PAYMENT PROCESSING", "Initiate the ACH transfer to the vendor"); + + // Step 7: Process Payment + bill = await Step7_ProcessBill(bill.BillId); + + // Step 8: Verify Payment + await Step8_VerifyPayment(bill.BillId); + + // ═══════════════════════════════════════════════════════════════════════════════════════ + // COMPLETE + // ═══════════════════════════════════════════════════════════════════════════════════════ + + PrintCompletion(vendor.VendorId, bill.BillId, testAmount, bill.PaymentRequestStatus); + } + + #region Workflow Steps + + private async Task Step1_CreateVendor(string vendorName) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 1: CREATE VENDOR ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Register a new vendor in the system before you can pay them."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Vendor │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Required Fields: │"); + _output.WriteLine(" │ • VendorName - Display name for the vendor │"); + _output.WriteLine(" │ • EmailForRemittance - Email to send payment notifications │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Payment Method (at least one required): │"); + _output.WriteLine(" │ • AchPaymentEnabled: true - Enable ACH bank transfers │"); + _output.WriteLine(" │ • VendorCardPaymentEnabled: true - Enable virtual card payments │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Optional Fields: │"); + _output.WriteLine(" │ • VendorAddress - Vendor's mailing address │"); + _output.WriteLine(" │ • VendorContact - Primary contact person │"); + _output.WriteLine(" │ • SendNotification - Email vendor when payments are made │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + var createVendorRequest = new CreateVendorRequestModel + { + VendorName = vendorName, + EmailForRemittance = "test@example.com", + AchPaymentEnabled = true, + VendorCardPaymentEnabled = false, + SendNotification = false, + VendorAddress = new VendorAddressModel + { + AddressLine1 = "123 Test Street", + City = "Test City", + State = "NY", + PostalCode = "10001" + }, + VendorContact = new VendorContactModel + { + FirstName = "Test", + LastName = "Contact", + Email = "contact@example.com" + } + }; + + _output.WriteLine(" ⏳ Creating vendor..."); + var vendor = await _pexApiClient.CreateVendor(_apiToken, createVendorRequest); + + _output.WriteLine($" ✅ Vendor created successfully!"); + _output.WriteLine(""); + PrintVendorState(vendor); + PrintVendorStateMachine(vendor.VendorStatus, vendor.VendorStatusTrigger); + + Assert.True(vendor.VendorId > 0); + return vendor; + } + + private async Task Step2_AddBankAccount(int vendorId) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 2: ADD BANK ACCOUNT ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Add ACH bank account details so payments can be sent to the vendor."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Vendor/{vendorId}/BankAccount │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Required Fields: │"); + _output.WriteLine(" │ • BankName - Name of the bank │"); + _output.WriteLine(" │ • BankRoutingNumber - 9-digit ABA routing number │"); + _output.WriteLine(" │ • BankAccountNumber - Account number │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Note: Bank account must be added before ACH payments can be processed. │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + var addBankRequest = new AddVendorBankAccountRequestModel + { + BankName = "Test Bank", + BankRoutingNumber = "021000021", + BankAccountNumber = "123456789" + }; + + _output.WriteLine(" ⏳ Adding bank account..."); + var vendor = await _pexApiClient.AddVendorBankAccount(_apiToken, vendorId, addBankRequest); + + _output.WriteLine($" ✅ Bank account added successfully!"); + _output.WriteLine($" Bank Accounts: {vendor.BankAccounts?.Count ?? 0}"); + _output.WriteLine(""); + + return vendor; + } + + private async Task Step3_ApproveVendor(VendorModel vendor) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 3: APPROVE VENDOR ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Activate the vendor so they can receive payments."); + _output.WriteLine(" Required only if your business has an Approval Policy workflow configured."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Vendor/{vendorId}/Approve │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ No request body required. │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Prerequisites: │"); + _output.WriteLine(" │ • Vendor must exist │"); + _output.WriteLine(" │ • User must have vendor approval permissions │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Result: Vendor status changes to Onboarded/Active │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + if (vendor.VendorStatus == VendorStatus.Onboarded) + { + _output.WriteLine(" ℹ️ Vendor already onboarded (no Approval Policy workflow configured)."); + _output.WriteLine(""); + PrintVendorState(vendor); + return vendor; + } + + try + { + _output.WriteLine(" ⏳ Approving vendor..."); + vendor = await _pexApiClient.ApproveVendor(_apiToken, vendor.VendorId); + _output.WriteLine($" ✅ Vendor approved successfully!"); + } + catch (PexCard.Api.Client.Core.Exceptions.PexApiClientException ex) + { + _output.WriteLine($" ⚠️ Could not approve vendor: {ex.Message}"); + vendor = await _pexApiClient.GetVendor(_apiToken, vendor.VendorId); + + if (vendor.VendorStatus != VendorStatus.Onboarded) + { + _output.WriteLine(""); + _output.WriteLine(" ❌ STOPPING: Vendor must be Onboarded to create bills."); + Assert.Fail($"Vendor approval required but failed: {ex.Message}"); + } + } + + _output.WriteLine(""); + PrintVendorState(vendor); + PrintVendorStateMachine(vendor.VendorStatus, vendor.VendorStatusTrigger); + + return vendor; + } + + private async Task Step4_CreateBill(int vendorId, decimal amount, string billRefNo) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 4: CREATE BILL ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Create a bill (payment request) to pay the vendor."); + _output.WriteLine(" A bill represents a payment you want to make to a vendor."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Bill │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Required Fields: │"); + _output.WriteLine(" │ • VendorId - ID of the vendor to pay │"); + _output.WriteLine(" │ • Amount - Payment amount in USD │"); + _output.WriteLine(" │ • PaymentMethod - ACH or VendorCard │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Optional Fields: │"); + _output.WriteLine(" │ • BillPayment.BillDate - Date of the invoice/bill │"); + _output.WriteLine(" │ • BillPayment.DueDate - When payment is due │"); + _output.WriteLine(" │ • BillPayment.BillRefNo - Your reference number for tracking │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Result: Bill created in Draft status │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + var createBillRequest = new CreateBillRequestModel + { + VendorId = vendorId, + Amount = amount, + PaymentMethod = BillPaymentMethodType.ACH, + BillPayment = new BillPaymentDetailsModel + { + BillDate = DateTimeOffset.Now.AddDays(-5), + DueDate = DateTimeOffset.Now.AddDays(25), + BillRefNo = billRefNo + } + }; + + _output.WriteLine(" ⏳ Creating bill..."); + var bill = await _pexApiClient.CreateBill(_apiToken, createBillRequest); + + _output.WriteLine($" ✅ Bill created successfully!"); + _output.WriteLine(""); + PrintBillState(bill); + PrintBillStateMachine(bill.PaymentRequestStatus); + + Assert.True(bill.BillId > 0); + Assert.Equal(amount, bill.Amount); + return bill; + } + + private async Task Step5_SubmitBill(int billId) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 5: SUBMIT BILL FOR APPROVAL ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Submit the bill for approval workflow processing."); + _output.WriteLine(" After submission, the bill enters your business's approval workflow."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Bill/{billId}/Submit │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ No request body required. │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Prerequisites: │"); + _output.WriteLine(" │ • Bill must be in Draft status │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Result: Bill moves to Pending/Submitted status │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + _output.WriteLine(" ⏳ Submitting bill for approval..."); + var bill = await _pexApiClient.SubmitBill(_apiToken, billId); + + _output.WriteLine($" ✅ Bill submitted successfully!"); + _output.WriteLine(""); + PrintBillState(bill); + PrintBillStateMachine(bill.PaymentRequestStatus); + + return bill; + } + + private async Task Step6_ApproveBill(BillModel bill) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 6: APPROVE BILL ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Approve the bill for payment processing."); + _output.WriteLine(" Required only if your business has an Approval Policy workflow configured."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Bill/{billId}/Approve │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Optional Fields: │"); + _output.WriteLine(" │ • Reason - Approval reason/notes │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Prerequisites: │"); + _output.WriteLine(" │ • Bill must be in Pending/Submitted status │"); + _output.WriteLine(" │ • User must have approval permissions │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Result: Bill moves to Approved status, ready for processing │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + if (bill.PaymentRequestStatus == "Approved" || bill.PaymentRequestStatus == "PendingPaymentTransfer") + { + _output.WriteLine(" ℹ️ Bill already approved (no Approval Policy workflow configured)."); + _output.WriteLine(""); + PrintBillState(bill); + return bill; + } + + _output.WriteLine(" ⏳ Approving bill..."); + bill = await _pexApiClient.ApproveBill(_apiToken, bill.BillId, new ApproveBillRequestModel + { + Reason = "SDK Integration Test approval" + }); + + _output.WriteLine($" ✅ Bill approved successfully!"); + _output.WriteLine(""); + PrintBillState(bill); + PrintBillStateMachine(bill.PaymentRequestStatus); + + return bill; + } + + private async Task Step7_ProcessBill(int billId) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 7: PROCESS PAYMENT ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Initiate the actual ACH transfer to the vendor's bank account."); + _output.WriteLine(" This step sends the payment to the ACH network for processing."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: POST │"); + _output.WriteLine(" │ Endpoint: /V4/Bill/{billId}/Process │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Optional Fields: │"); + _output.WriteLine(" │ • ProcessingDate - Date to process (defaults to today) │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Prerequisites: │"); + _output.WriteLine(" │ • Bill must be Approved │"); + _output.WriteLine(" │ • Vendor must have valid bank account │"); + _output.WriteLine(" │ • Business must have sufficient balance │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Result: ACH transfer initiated, bill moves to Processing status │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Note: ACH transfers typically take 1-3 business days to complete. │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + // Refresh bill state before processing + var bill = await _pexApiClient.GetBill(_apiToken, billId); + _output.WriteLine($" 📋 Bill state before processing:"); + _output.WriteLine($" Status: {bill.PaymentRequestStatus} / {bill.PaymentRequestStatusTrigger}"); + _output.WriteLine(""); + + _output.WriteLine(" ⏳ Processing payment..."); + try + { + bill = await _pexApiClient.ProcessBill(_apiToken, billId); + _output.WriteLine($" ✅ Payment processed successfully!"); + } + catch (PexCard.Api.Client.Core.Exceptions.PexApiClientException ex) + { + _output.WriteLine($" ❌ FAILED: {ex.Message}"); + _output.WriteLine($" HTTP Status: {(int)ex.Code} {ex.Code}"); + _output.WriteLine(""); + _output.WriteLine(" Common reasons for failure:"); + _output.WriteLine(" • Bill not in Approved status"); + _output.WriteLine(" • Vendor bank account not valid"); + _output.WriteLine(" • Insufficient business balance"); + _output.WriteLine(" • ACH processing not enabled"); + throw; + } + + _output.WriteLine(""); + PrintBillState(bill); + PrintBillStateMachine(bill.PaymentRequestStatus); + + return bill; + } + + private async Task Step8_VerifyPayment(int billId) + { + _output.WriteLine(""); + _output.WriteLine("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"); + _output.WriteLine("┃ STEP 8: VERIFY PAYMENT ┃"); + _output.WriteLine("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"); + _output.WriteLine(""); + _output.WriteLine(" PURPOSE: Retrieve payment records to verify the ACH transfer was initiated."); + _output.WriteLine(""); + _output.WriteLine(" ┌─ API DETAILS ────────────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ Method: GET │"); + _output.WriteLine(" │ Endpoint: /V4/Bill/{billId}/Payments │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Returns: List of payment records associated with the bill │"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ Payment Status Values: │"); + _output.WriteLine(" │ • Pending - Payment created, not yet sent │"); + _output.WriteLine(" │ • Processing - Sent to ACH network │"); + _output.WriteLine(" │ • Completed - Successfully delivered │"); + _output.WriteLine(" │ • Failed - Transfer failed (NSF, invalid account, etc.) │"); + _output.WriteLine(" │ • Returned - Returned by receiving bank │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + _output.WriteLine(" ⏳ Fetching payment records..."); + var payments = await _pexApiClient.GetBillPayments(_apiToken, billId); + + _output.WriteLine($" ✅ Payment records retrieved!"); + _output.WriteLine(""); + _output.WriteLine($" Payment Count: {payments.Payments?.Count ?? 0}"); + _output.WriteLine(""); + + if (payments.Payments != null && payments.Payments.Count > 0) + { + _output.WriteLine(" ┌─ PAYMENT RECORDS ────────────────────────────────────────────────────────────┐"); + foreach (var payment in payments.Payments) + { + _output.WriteLine($" │ Payment ID: {payment.PaymentId,-20} Amount: ${payment.Amount,-15} Status: {payment.PaymentStatus,-10} │"); + } + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + } + _output.WriteLine(""); + } + + #endregion + + #region Output Helpers + + private void PrintHeader() + { + _output.WriteLine(""); + _output.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════╗"); + _output.WriteLine("║ ║"); + _output.WriteLine("║ PEX BILL PAY API - COMPLETE WORKFLOW DEMONSTRATION ║"); + _output.WriteLine("║ ║"); + _output.WriteLine("║ This test demonstrates the full flow for paying vendors via ACH. ║"); + _output.WriteLine("║ Use this as a reference for implementing Bill Pay in your application. ║"); + _output.WriteLine("║ ║"); + _output.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════╝"); + _output.WriteLine(""); + } + + private void PrintWorkflowOverview() + { + _output.WriteLine("┌──────────────────────────────────────────────────────────────────────────────────┐"); + _output.WriteLine("│ WORKFLOW OVERVIEW │"); + _output.WriteLine("├──────────────────────────────────────────────────────────────────────────────────┤"); + _output.WriteLine("│ │"); + _output.WriteLine("│ PHASE 1: VENDOR SETUP (One-time per vendor) │"); + _output.WriteLine("│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │"); + _output.WriteLine("│ │ 1. Create │───►│ 2. Add Bank │───►│ 3. Approve │ │"); + _output.WriteLine("│ │ Vendor │ │ Account │ │ Vendor* │ │"); + _output.WriteLine("│ └─────────────┘ └─────────────┘ └─────────────┘ │"); + _output.WriteLine("│ │ │"); + _output.WriteLine("│ ▼ │"); + _output.WriteLine("│ PHASE 2: CREATE BILL PHASE 3: APPROVAL WORKFLOW │"); + _output.WriteLine("│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │"); + _output.WriteLine("│ │ 4. Create │───────────────────────►│ 5. Submit │───►│ 6. Approve │ │"); + _output.WriteLine("│ │ Bill │ │ Bill │ │ Bill* │ │"); + _output.WriteLine("│ └─────────────┘ └─────────────┘ └─────────────┘ │"); + _output.WriteLine("│ │ │"); + _output.WriteLine("│ ▼ │"); + _output.WriteLine("│ PHASE 4: PAYMENT PROCESSING │"); + _output.WriteLine("│ ┌─────────────┐ ┌─────────────┐ │"); + _output.WriteLine("│ │ 7. Process │───►│ 8. Verify │ * Required only if business has an │"); + _output.WriteLine("│ │ Payment │ │ Payment │ Approval Policy workflow configured │"); + _output.WriteLine("│ └─────────────┘ └─────────────┘ │"); + _output.WriteLine("│ │ │"); + _output.WriteLine("│ └───────────► ACH transfer sent to vendor's bank (1-3 business days) │"); + _output.WriteLine("│ │"); + _output.WriteLine("│ Repeat steps 4-8 for each bill payment to the same vendor. │"); + _output.WriteLine("│ │"); + _output.WriteLine("└──────────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + } + + private async Task PrintBusinessContext() + { + _output.WriteLine("┌──────────────────────────────────────────────────────────────────────────────────┐"); + _output.WriteLine("│ AUTHENTICATION & BUSINESS CONTEXT │"); + _output.WriteLine("└──────────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + + var tokenData = await _pexApiClient.GetToken(_apiToken); + var businessDetails = await _pexApiClient.GetBusinessDetails(_apiToken); + + _output.WriteLine(" ┌─ TOKEN ──────────────────────────────────────────────────────────────────────┐"); + _output.WriteLine($" │ App ID: {tokenData.AppId,-60} │"); + _output.WriteLine($" │ Platform ID: {tokenData.PlatformAccountId?.ToString() ?? "(null)",-60} │"); + _output.WriteLine($" │ User Type: {tokenData.UserType,-60} │"); + _output.WriteLine($" │ Expires: {tokenData.TokenExpiration:yyyy-MM-dd HH:mm:ss,-49} │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + _output.WriteLine(" ┌─ BUSINESS ───────────────────────────────────────────────────────────────────┐"); + _output.WriteLine($" │ ID: {businessDetails.BusinessAccountId,-60} │"); + _output.WriteLine($" │ Name: {businessDetails.BusinessName,-60} │"); + _output.WriteLine($" │ Status: {businessDetails.BusinessAccountStatus,-60} │"); + _output.WriteLine($" │ Balance: ${businessDetails.BusinessAccountBalance,-59} │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + } + + private void PrintPhaseHeader(string phase, string description) + { + _output.WriteLine(""); + _output.WriteLine("══════════════════════════════════════════════════════════════════════════════════"); + _output.WriteLine($" {phase}"); + _output.WriteLine($" {description}"); + _output.WriteLine("══════════════════════════════════════════════════════════════════════════════════"); + } + + private void PrintVendorState(VendorModel vendor) + { + _output.WriteLine(" ┌─ VENDOR STATE ───────────────────────────────────────────────────────────────┐"); + _output.WriteLine($" │ Vendor ID: {vendor.VendorId,-60} │"); + _output.WriteLine($" │ Name: {vendor.VendorName,-60} │"); + _output.WriteLine($" │ Status: {vendor.VendorStatus,-60} │"); + _output.WriteLine($" │ Status Trigger:{vendor.VendorStatusTrigger,-60} │"); + _output.WriteLine($" │ Bank Accounts: {vendor.BankAccounts?.Count ?? 0,-60} │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + } + + private void PrintVendorStateMachine(VendorStatus currentStatus, VendorStatusTrigger trigger) + { + _output.WriteLine(" ┌─ VENDOR STATE MACHINE ───────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ │"); + var draftMarker = currentStatus == VendorStatus.Draft ? ">>>" : " "; + var pendingMarker = currentStatus == VendorStatus.Pending ? ">>>" : " "; + var onboardedMarker = currentStatus == VendorStatus.Onboarded ? ">>>" : " "; + _output.WriteLine($" │ {draftMarker} [Draft] ──── Submit ────► [Pending] ──── Approve ────► [Onboarded] {onboardedMarker}│"); + _output.WriteLine(" │ │ │"); + _output.WriteLine(" │ └──── Reject ────► [Closed] │"); + _output.WriteLine(" │ │"); + _output.WriteLine($" │ Current: {currentStatus} / {trigger,-53} │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + } + + private void PrintBillState(BillModel bill) + { + _output.WriteLine(" ┌─ BILL STATE ─────────────────────────────────────────────────────────────────┐"); + _output.WriteLine($" │ Bill ID: {bill.BillId,-60} │"); + _output.WriteLine($" │ Amount: ${bill.Amount,-59} │"); + _output.WriteLine($" │ Status: {bill.PaymentRequestStatus,-60} │"); + _output.WriteLine($" │ Status Trigger:{bill.PaymentRequestStatusTrigger,-60} │"); + _output.WriteLine($" │ Merchant: {bill.MerchantName ?? "(not set)",-60} │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + } + + private void PrintBillStateMachine(string currentStatus) + { + _output.WriteLine(" ┌─ BILL STATE MACHINE ─────────────────────────────────────────────────────────┐"); + _output.WriteLine(" │ │"); + _output.WriteLine(" │ [Draft] ──► [Submitted] ──► [Approved] ──► [Processing] ──► [Completed] │"); + _output.WriteLine(" │ │ │ │ │ │"); + _output.WriteLine(" │ │ └── Reject ──► [Rejected] └── Fail ──► [Failed] │"); + _output.WriteLine(" │ │ │"); + _output.WriteLine(" │ └── Cancel ──► [Cancelled] │"); + _output.WriteLine(" │ │"); + _output.WriteLine($" │ Current: {currentStatus,-65} │"); + _output.WriteLine(" └──────────────────────────────────────────────────────────────────────────────┘"); + _output.WriteLine(""); + } + + private void PrintCompletion(int vendorId, int billId, decimal amount, string finalStatus) + { + _output.WriteLine(""); + _output.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════╗"); + _output.WriteLine("║ ║"); + _output.WriteLine("║ ✅ BILL PAY WORKFLOW COMPLETED SUCCESSFULLY! ║"); + _output.WriteLine("║ ║"); + _output.WriteLine("╠══════════════════════════════════════════════════════════════════════════════════╣"); + _output.WriteLine("║ ║"); + _output.WriteLine($"║ Vendor ID: {vendorId,-61} ║"); + _output.WriteLine($"║ Bill ID: {billId,-61} ║"); + _output.WriteLine($"║ Amount: ${amount,-60} ║"); + _output.WriteLine($"║ Final Status: {finalStatus,-61} ║"); + _output.WriteLine("║ ║"); + _output.WriteLine("╠══════════════════════════════════════════════════════════════════════════════════╣"); + _output.WriteLine("║ ║"); + _output.WriteLine("║ NEXT STEPS: ║"); + _output.WriteLine("║ • ACH transfers typically complete in 1-3 business days ║"); + _output.WriteLine("║ • Use GET /V4/Bill/{billId}/Payments to check payment status ║"); + _output.WriteLine("║ • Set up webhooks to receive real-time payment status updates ║"); + _output.WriteLine("║ ║"); + _output.WriteLine("║ RESOURCES: ║"); + _output.WriteLine("║ • API Documentation: https://developer.pexcard.com ║"); + _output.WriteLine("║ • SDK Repository: https://github.com/pexcard/pex-sdk-dotnet ║"); + _output.WriteLine("║ ║"); + _output.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════╝"); + _output.WriteLine(""); + } + + #endregion + } +} diff --git a/tests/PexCard.Api.Client.Core.Tests/PexCard.Api.Client.Core.Tests.csproj b/tests/PexCard.Api.Client.Core.Tests/PexCard.Api.Client.Core.Tests.csproj index f4f9ce9..b9c477f 100644 --- a/tests/PexCard.Api.Client.Core.Tests/PexCard.Api.Client.Core.Tests.csproj +++ b/tests/PexCard.Api.Client.Core.Tests/PexCard.Api.Client.Core.Tests.csproj @@ -7,6 +7,8 @@ + + @@ -20,6 +22,15 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/tests/PexCard.Api.Client.Core.Tests/appsettings.json b/tests/PexCard.Api.Client.Core.Tests/appsettings.json new file mode 100644 index 0000000..4243dc2 --- /dev/null +++ b/tests/PexCard.Api.Client.Core.Tests/appsettings.json @@ -0,0 +1,6 @@ +{ + "PexApi": { + "BaseUrl": "https://sand-coreapi.pexcard.com", + "Token": "YOUR_TOKEN_HERE" + } +} From f7ab460ffb87a4039d488a2e7d0b9d62ecd09779 Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Thu, 18 Dec 2025 12:28:52 -0500 Subject: [PATCH 2/3] Update query param casing and use invariant culture for amounts Updated query parameter names to PascalCase for API consistency. Switched "AmountFrom" and "AmountTo" to use invariant culture formatting. Added System.Globalization import for support. --- src/PexCard.Api.Client/PexApiClient.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/PexCard.Api.Client/PexApiClient.cs b/src/PexCard.Api.Client/PexApiClient.cs index b94f02c..90f2af4 100644 --- a/src/PexCard.Api.Client/PexApiClient.cs +++ b/src/PexCard.Api.Client/PexApiClient.cs @@ -9,6 +9,7 @@ using PexCard.Api.Client.Models; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -1024,14 +1025,14 @@ public async Task GetVendors(string externalToken, GetV if (request.CardholderAcctId.HasValue) { - requestUriQueryParams.Add("cardholderAcctId", request.CardholderAcctId.Value.ToString()); + requestUriQueryParams.Add("CardholderAcctId", request.CardholderAcctId.Value.ToString()); } if (request.VendorStatuses != null) { foreach (var status in request.VendorStatuses) { - requestUriQueryParams.Add("vendorStatuses", status.ToString()); + requestUriQueryParams.Add("VendorStatuses", status.ToString()); } } @@ -1039,12 +1040,12 @@ public async Task GetVendors(string externalToken, GetV { foreach (var trigger in request.VendorStatusTriggers) { - requestUriQueryParams.Add("vendorStatusTriggers", trigger.ToString()); + requestUriQueryParams.Add("VendorStatusTriggers", trigger.ToString()); } } - requestUriQueryParams.Add("pageIndex", request.PageIndex.ToString()); - requestUriQueryParams.Add("pageSize", request.PageSize.ToString()); + requestUriQueryParams.Add("PageIndex", request.PageIndex.ToString()); + requestUriQueryParams.Add("PageSize", request.PageSize.ToString()); requestUriBuilder.Query = requestUriQueryParams.ToString(); var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUriBuilder.Uri); @@ -1315,9 +1316,9 @@ public async Task SearchBills(string externalToken, Se if (request.VendorId.HasValue) requestUriQueryParams.Add("VendorId", request.VendorId.Value.ToString()); if (request.AmountFrom.HasValue) - requestUriQueryParams.Add("AmountFrom", request.AmountFrom.Value.ToString()); + requestUriQueryParams.Add("AmountFrom", request.AmountFrom.Value.ToString(CultureInfo.InvariantCulture)); if (request.AmountTo.HasValue) - requestUriQueryParams.Add("AmountTo", request.AmountTo.Value.ToString()); + requestUriQueryParams.Add("AmountTo", request.AmountTo.Value.ToString(CultureInfo.InvariantCulture)); if (request.PaymentRequestStatuses != null) { foreach (var status in request.PaymentRequestStatuses) From 3438ae40379b1beef49ab1e29b3689a64047cf1b Mon Sep 17 00:00:00 2001 From: Mike Dunn Date: Thu, 18 Dec 2025 12:53:10 -0500 Subject: [PATCH 3/3] Update PexApi BaseUrl to sandbox-coreapi.pexcard.com Changed the PexApi BaseUrl in appsettings.json from sand-coreapi.pexcard.com to sandbox-coreapi.pexcard.com to use the correct sandbox environment. --- tests/PexCard.Api.Client.Core.Tests/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PexCard.Api.Client.Core.Tests/appsettings.json b/tests/PexCard.Api.Client.Core.Tests/appsettings.json index 4243dc2..5b3a7f4 100644 --- a/tests/PexCard.Api.Client.Core.Tests/appsettings.json +++ b/tests/PexCard.Api.Client.Core.Tests/appsettings.json @@ -1,6 +1,6 @@ { "PexApi": { - "BaseUrl": "https://sand-coreapi.pexcard.com", + "BaseUrl": "https://sandbox-coreapi.pexcard.com", "Token": "YOUR_TOKEN_HERE" } }