|
|
@@ -0,0 +1,246 @@
|
|
|
+using Comal.Classes;
|
|
|
+using InABox.Core;
|
|
|
+using InABox.Core.Postable;
|
|
|
+using InABox.Poster.Xero;
|
|
|
+using InABox.Scripting;
|
|
|
+using PRS.Shared.Posters.Xero.Services;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using System.Text;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using System.Windows.Markup.Localizer;
|
|
|
+using Bill = Comal.Classes.Bill;
|
|
|
+using XeroPayment = Xero.NetStandard.OAuth2.Model.Accounting.BatchPayment;
|
|
|
+using XeroBillPayment = Xero.NetStandard.OAuth2.Model.Accounting.Payment;
|
|
|
+
|
|
|
+namespace PRS.Shared.Posters.Xero;
|
|
|
+
|
|
|
+public class PaymentXeroPosterSettings : XeroPosterSettings
|
|
|
+{
|
|
|
+ [EditorSequence(1)]
|
|
|
+ [Comment("The Xero account code for payments")]
|
|
|
+ [TextBoxEditor]
|
|
|
+ public string Account { get; set; }
|
|
|
+
|
|
|
+ protected override string DefaultScript()
|
|
|
+ {
|
|
|
+ return @"
|
|
|
+using XeroPayment = Xero.NetStandard.OAuth2.Model.Accounting.BatchPayment;
|
|
|
+using XeroBillPayment = Xero.NetStandard.OAuth2.Model.Accounting.Payment;
|
|
|
+
|
|
|
+// 'Module' *must* implement " + nameof(IPaymentXeroPosterScript) + @"
|
|
|
+public class Module : " + nameof(IPaymentXeroPosterScript) + @"
|
|
|
+{
|
|
|
+ public void " + nameof(IPaymentXeroPosterScript.BeforePost) + @"(IDataModel<Payment> model)
|
|
|
+ {
|
|
|
+ // Perform pre-processing
|
|
|
+ }
|
|
|
+
|
|
|
+ public void " + nameof(IPaymentXeroPosterScript.ProcessPayment) + @"(IDataModel<Payment> model, Payment payment, XeroPayment xeroPayment)
|
|
|
+ {
|
|
|
+ // Do extra processing for a payment; throw an exception to fail this payment.
|
|
|
+ }
|
|
|
+
|
|
|
+ public void " + nameof(IPaymentXeroPosterScript.ProcessBillPayment) + @"(IDataModel<Payment> model, BillPayment billPayment, XeroBillPayment xeroBillPayment)
|
|
|
+ {
|
|
|
+ // Do extra processing for an bill payment; throw an exception to fail this bill payment (and thus fail the entire payment)
|
|
|
+ }
|
|
|
+}";
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public interface IPaymentXeroPosterScript
|
|
|
+{
|
|
|
+ void BeforePost(IDataModel<Payment> model);
|
|
|
+
|
|
|
+ void ProcessPayment(IDataModel<Payment> model, Payment payment, XeroPayment xeroPayment);
|
|
|
+
|
|
|
+ void ProcessBillPayment(IDataModel<Payment> model, BillPayment billPayment, XeroBillPayment xeroBillPayment);
|
|
|
+}
|
|
|
+
|
|
|
+public class PaymentXeroPoster : IXeroPoster<Payment, PaymentXeroPosterSettings>
|
|
|
+{
|
|
|
+ public ScriptDocument? Script { get; set; }
|
|
|
+ public XeroConnectionData ConnectionData { get; set; }
|
|
|
+ public PaymentXeroPosterSettings Settings { get; set; }
|
|
|
+ public IPosterDispatcher Dispatcher { get; set; }
|
|
|
+
|
|
|
+ public IPaymentXeroPosterScript? ScriptObject => Script?.GetObject<IPaymentXeroPosterScript>();
|
|
|
+
|
|
|
+ public bool BeforePost(IDataModel<Payment> model)
|
|
|
+ {
|
|
|
+ foreach(var (_, table) in model.ModelTables)
|
|
|
+ {
|
|
|
+ table.IsDefault = false;
|
|
|
+ }
|
|
|
+ model.SetIsDefault<Payment>(true);
|
|
|
+ model.SetColumns<Payment>(RequiredPaymentColumns());
|
|
|
+
|
|
|
+ model.SetIsDefault<BillPayment>(true, alias: "Payment_BillPayment");
|
|
|
+ model.SetColumns<BillPayment>(RequiredBillPaymentColumns(), alias: "Payment_BillPayment");
|
|
|
+
|
|
|
+ ScriptObject?.BeforePost(model);
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ #region Script Functions
|
|
|
+
|
|
|
+ private Result<Exception> ProcessPayment(IDataModel<Payment> model, Payment payment, XeroPayment xeroPayment)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ ScriptObject?.ProcessPayment(model, payment, xeroPayment);
|
|
|
+ return Result.Ok();
|
|
|
+ }
|
|
|
+ catch(Exception e)
|
|
|
+ {
|
|
|
+ return Result.Error(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private Result<Exception> ProcessBillPayment(IDataModel<Payment> model, BillPayment billPayment, XeroBillPayment xeroBillPayment)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ ScriptObject?.ProcessBillPayment(model, billPayment, xeroBillPayment);
|
|
|
+ return Result.Ok();
|
|
|
+ }
|
|
|
+ catch(Exception e)
|
|
|
+ {
|
|
|
+ return Result.Error(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ private static Columns<Payment> RequiredPaymentColumns()
|
|
|
+ {
|
|
|
+ return Columns.None<Payment>()
|
|
|
+ .Add(x => x.ID)
|
|
|
+ .Add(x => x.PostedReference)
|
|
|
+ .Add(x => x.PostedStatus)
|
|
|
+ .Add(x => x.Date)
|
|
|
+ .Add(x => x.Notes);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Columns<BillPayment> RequiredBillPaymentColumns()
|
|
|
+ {
|
|
|
+ return Columns.None<BillPayment>()
|
|
|
+ .Add(x => x.ID)
|
|
|
+ .Add(x => x.Amount)
|
|
|
+ .Add(x => x.Payment.ID)
|
|
|
+ .Add(x => x.Bill.Number)
|
|
|
+ .Add(x => x.Bill.PostedReference);
|
|
|
+ }
|
|
|
+
|
|
|
+ public IPostResult<Payment> Process(IDataModel<Payment> model)
|
|
|
+ {
|
|
|
+ // https://developer.xero.com/documentation/api/accounting/payments
|
|
|
+
|
|
|
+ var results = new PostResult<Payment>();
|
|
|
+
|
|
|
+ var service = new PaymentService(ConnectionData);
|
|
|
+
|
|
|
+ var payments = model.GetTable<Payment>().ToArray<Payment>();
|
|
|
+
|
|
|
+ var billPayments = model.GetTable<BillPayment>("Payment_BillPayment")
|
|
|
+ .ToObjects<BillPayment>().GroupByDictionary(x => x.Payment.ID);
|
|
|
+
|
|
|
+ foreach(var payment in payments)
|
|
|
+ {
|
|
|
+ // For payments, always create a new one, so we cannot posted already posted stuff.
|
|
|
+ if(payment.PostedStatus == PostedStatus.Posted)
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ var xeroPayment = new XeroPayment();
|
|
|
+
|
|
|
+ if (Settings.Account.IsNullOrWhiteSpace())
|
|
|
+ {
|
|
|
+ throw new MissingSettingException<PaymentXeroPosterSettings>(x => x.Account);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ xeroPayment.Account.Code = Settings.Account;
|
|
|
+ }
|
|
|
+
|
|
|
+ xeroPayment.Date = payment.Date;
|
|
|
+ xeroPayment.Details = payment.Notes.Truncate(18);
|
|
|
+
|
|
|
+ if(billPayments.TryGetValue(payment.ID, out var paymentBills))
|
|
|
+ {
|
|
|
+ xeroPayment.Payments.Clear();
|
|
|
+ string? failed = null;
|
|
|
+ for(int i = 0; i < paymentBills.Count; ++i)
|
|
|
+ {
|
|
|
+ var bill = paymentBills[i];
|
|
|
+
|
|
|
+ var line = new XeroBillPayment();
|
|
|
+ // BankAccountNumber
|
|
|
+
|
|
|
+ line.Amount = bill.Amount;
|
|
|
+ line.Details = payment.Notes;
|
|
|
+
|
|
|
+ if(Guid.TryParse(bill.Bill.PostedReference, out var xeroBillID))
|
|
|
+ {
|
|
|
+ line.Invoice.InvoiceID = xeroBillID;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ failed = $"Bill {bill.Bill.Number} hasn't been posted yet; it must be posted before this payment can be.";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(!ProcessBillPayment(model, bill, line).Get(out var error))
|
|
|
+ {
|
|
|
+ failed = error.Message;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ xeroPayment.Payments.Add(line);
|
|
|
+ }
|
|
|
+
|
|
|
+ if(failed is not null)
|
|
|
+ {
|
|
|
+ results.AddFailed(payment, failed);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ xeroPayment.Payments.Clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ if(!ProcessPayment(model, payment, xeroPayment).Get(out var e))
|
|
|
+ {
|
|
|
+ results.AddFailed(payment, e.Message);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(service.Save(xeroPayment).Get(out var result, out e))
|
|
|
+ {
|
|
|
+ payment.PostedReference = result.BatchPaymentID.ToString() ?? "";
|
|
|
+ results.AddSuccess(payment);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ CoreUtils.LogException("", e, $"Error while posting payment {payment.ID}");
|
|
|
+ results.AddFailed(payment, e.Message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public class PaymentXeroPosterEngine<T> : XeroPosterEngine<Payment, PaymentXeroPoster, PaymentXeroPosterSettings>
|
|
|
+{
|
|
|
+ protected override IList<string> RequiredScopes()
|
|
|
+ {
|
|
|
+ return [
|
|
|
+ "accounting.transactions"
|
|
|
+ ];
|
|
|
+ }
|
|
|
+}
|