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" + } +}