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..90f2af4 100644
--- a/src/PexCard.Api.Client/PexApiClient.cs
+++ b/src/PexCard.Api.Client/PexApiClient.cs
@@ -9,9 +9,12 @@
using PexCard.Api.Client.Models;
using System;
using System.Collections.Generic;
+using System.Globalization;
+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 +1014,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(CultureInfo.InvariantCulture));
+ if (request.AmountTo.HasValue)
+ requestUriQueryParams.Add("AmountTo", request.AmountTo.Value.ToString(CultureInfo.InvariantCulture));
+ 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..5b3a7f4
--- /dev/null
+++ b/tests/PexCard.Api.Client.Core.Tests/appsettings.json
@@ -0,0 +1,6 @@
+{
+ "PexApi": {
+ "BaseUrl": "https://sandbox-coreapi.pexcard.com",
+ "Token": "YOUR_TOKEN_HERE"
+ }
+}