openmeter / billing /app.go
Leon4gr45's picture
Upload folder using huggingface_hub
429334c verified
Raw
History Blame Contribute Delete
11.8 kB
package billing
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/samber/lo"
"github.com/samber/mo"
"github.com/openmeterio/openmeter/openmeter/app"
"github.com/openmeterio/openmeter/pkg/models"
)
type UpsertResults struct {
invoiceNumber string
externalID string
lineExternalIDs map[string]string
lineDiscountExternalIDs map[string]string
}
func NewUpsertResults() *UpsertResults {
return &UpsertResults{
lineExternalIDs: make(map[string]string),
lineDiscountExternalIDs: make(map[string]string),
}
}
func (u *UpsertResults) GetInvoiceNumber() (string, bool) {
return u.invoiceNumber, u.invoiceNumber != ""
}
func (u *UpsertResults) SetInvoiceNumber(invoiceNumber string) *UpsertResults {
u.invoiceNumber = invoiceNumber
return u
}
func (u *UpsertResults) GetExternalID() (string, bool) {
return u.externalID, u.externalID != ""
}
func (u *UpsertResults) SetExternalID(externalID string) *UpsertResults {
u.externalID = externalID
return u
}
func (u *UpsertResults) AddLineExternalID(lineID string, externalID string) *UpsertResults {
u.lineExternalIDs[lineID] = externalID
return u
}
func (u *UpsertResults) GetLineExternalID(lineID string) (string, bool) {
externalID, ok := u.lineExternalIDs[lineID]
return externalID, ok
}
func (u *UpsertResults) GetLineExternalIDs() map[string]string {
return u.lineExternalIDs
}
func (u *UpsertResults) AddLineDiscountExternalID(lineDiscountID string, externalID string) *UpsertResults {
u.lineDiscountExternalIDs[lineDiscountID] = externalID
return u
}
func (u *UpsertResults) GetLineDiscountExternalID(lineDiscountID string) (string, bool) {
externalID, ok := u.lineDiscountExternalIDs[lineDiscountID]
return externalID, ok
}
func (u *UpsertResults) GetLineDiscountExternalIDs() map[string]string {
return u.lineDiscountExternalIDs
}
type UpsertStandardInvoiceResult = UpsertResults
func NewUpsertStandardInvoiceResult() *UpsertStandardInvoiceResult {
return NewUpsertResults()
}
type FinalizeStandardInvoiceResult struct {
invoiceNumber string
paymentExternalID string
sentToCustomerAt mo.Option[time.Time]
}
func NewFinalizeStandardInvoiceResult() *FinalizeStandardInvoiceResult {
return &FinalizeStandardInvoiceResult{}
}
func (f *FinalizeStandardInvoiceResult) GetPaymentExternalID() (string, bool) {
return f.paymentExternalID, f.paymentExternalID != ""
}
func (f *FinalizeStandardInvoiceResult) SetPaymentExternalID(paymentExternalID string) *FinalizeStandardInvoiceResult {
f.paymentExternalID = paymentExternalID
return f
}
func (u *FinalizeStandardInvoiceResult) GetInvoiceNumber() (string, bool) {
return u.invoiceNumber, u.invoiceNumber != ""
}
func (f *FinalizeStandardInvoiceResult) SetInvoiceNumber(invoiceNumber string) *FinalizeStandardInvoiceResult {
f.invoiceNumber = invoiceNumber
return f
}
func (f *FinalizeStandardInvoiceResult) GetSentToCustomerAt() (time.Time, bool) {
return f.sentToCustomerAt.OrEmpty(), f.sentToCustomerAt.IsPresent()
}
func (f *FinalizeStandardInvoiceResult) SetSentToCustomerAt(sentToCustomerAt time.Time) *FinalizeStandardInvoiceResult {
f.sentToCustomerAt = mo.Some(sentToCustomerAt)
return f
}
func (f *FinalizeStandardInvoiceResult) MergeIntoInvoice(invoice *StandardInvoice) error {
if paymentExternalID, ok := f.GetPaymentExternalID(); ok {
invoice.ExternalIDs.Payment = paymentExternalID
}
if invoiceNumber, ok := f.GetInvoiceNumber(); ok {
invoice.Number = invoiceNumber
}
if sentToCustomerAt, ok := f.GetSentToCustomerAt(); ok {
invoice.SentToCustomerAt = &sentToCustomerAt
}
return nil
}
type PostAdvanceHookResult struct {
trigger *InvoiceTriggerInput
}
func NewPostAdvanceHookResult() *PostAdvanceHookResult {
return &PostAdvanceHookResult{}
}
func (p *PostAdvanceHookResult) InvokeTrigger(trigger InvoiceTriggerInput) *PostAdvanceHookResult {
p.trigger = &trigger
return p
}
func (p *PostAdvanceHookResult) GetTriggerToInvoke() *InvoiceTriggerInput {
return p.trigger
}
// InvoicingApp is the interface that should be implemented by the app to handle the invoicing
//
// apps can also implement InvoicingAppPostAdvanceHook to perform additional actions after the invoice
// has been advanced
// Warning: The received invoice is
// - read-only (e.g. any changes made to it are lost to prevent manipulation of the invoice state)
// - reflects the current in memory state of the invoice, thus if you fetched from the db
// an earlier version of the invoice will be passed, thus do not call any billingService methods
// from these callbacks.
type InvoicingApp interface {
// ValidateStandardInvoice validates if the app can run for the given invoice
ValidateStandardInvoice(ctx context.Context, invoice StandardInvoice) error
// UpsertStandardInvoice upserts the invoice on the remote system, the invoice is read-only, the app should not modify it
// the recommended behavior is that the invoices FlattenLinesByID is used to get all lines, then the app should
// synchronize all the fee lines and store the external IDs in the result.
UpsertStandardInvoice(ctx context.Context, invoice StandardInvoice) (*UpsertStandardInvoiceResult, error)
// FinalizeStandardInvoice finalizes the invoice on the remote system, starts the payment flow. It is safe to assume
// that the state machine have already performed an upsert as part of this state transition.
//
// If the payment is handled by a decoupled implementation (different app or app has strict separation of concerns)
// then the payment app will be called with FinalizePayment and that should return the external ID of the payment. (later)
FinalizeStandardInvoice(ctx context.Context, invoice StandardInvoice) (*FinalizeStandardInvoiceResult, error)
// DeleteStandardInvoice deletes the invoice on the remote system, the invoice is read-only, the app should not modify it
// the invoice deletion is only invoked for non-finalized invoices.
DeleteStandardInvoice(ctx context.Context, invoice StandardInvoice) error
}
type InvoicingAppPostAdvanceHook interface {
// PostAdvanceInvoiceHook is called after the invoice has been advanced to the next stable state
// (e.g. no next trigger is available)
//
// Can be used by the app to perform additional actions in case there are some post-processing steps
// required on the invoice.
PostAdvanceStandardInvoiceHook(ctx context.Context, invoice StandardInvoice) (*PostAdvanceHookResult, error)
}
// InvoicingAppAsyncSyncer is an optional interface that can be implemented by the app to support
// asynchronous syncing of the invoice (e.g. when we are receiving the payload such as with custominvoicing app)
type InvoicingAppAsyncSyncer interface {
CanDraftSyncAdvance(invoice StandardInvoice) (bool, error)
CanIssuingSyncAdvance(invoice StandardInvoice) (bool, error)
// TODO: finalization check
}
// GetApp returns the app from the app entity
func GetApp(app app.App) (InvoicingApp, error) {
customerApp, ok := app.(InvoicingApp)
if !ok {
return nil, AppError{
AppID: app.GetID(),
AppType: app.GetType(),
Err: fmt.Errorf("is not an invoicing app"),
}
}
return customerApp, nil
}
// MergeIntoInvoice merges the upsert invoice result into the invoice.
func (r UpsertStandardInvoiceResult) MergeIntoInvoice(invoice *StandardInvoice) error {
// Let's merge the results into the invoice
if invoiceNumber, ok := r.GetInvoiceNumber(); ok {
invoice.Number = invoiceNumber
}
if externalID, ok := r.GetExternalID(); ok {
invoice.ExternalIDs.Invoicing = externalID
}
if !invoice.Lines.IsPresent() {
return errors.New("invoice has no expanded lines")
}
var outErr error
// Let's merge the line IDs
lineIDToExternalID := r.GetLineExternalIDs()
dicountIDToExternalID := r.GetLineDiscountExternalIDs()
lines := invoice.Lines.OrEmpty()
for _, line := range lines {
if externalID, ok := lineIDToExternalID[line.ID]; ok {
line.ExternalIDs.Invoicing = externalID
delete(lineIDToExternalID, line.ID)
}
foundIDs := line.SetDiscountExternalIDs(dicountIDToExternalID)
for _, id := range foundIDs {
delete(dicountIDToExternalID, id)
}
for idx, detailedLine := range line.DetailedLines {
if externalID, ok := lineIDToExternalID[detailedLine.ID]; ok {
line.DetailedLines[idx].ExternalIDs.Invoicing = externalID
delete(lineIDToExternalID, detailedLine.ID)
}
foundIDs := line.DetailedLines[idx].SetDiscountExternalIDs(dicountIDToExternalID)
for _, id := range foundIDs {
delete(dicountIDToExternalID, id)
}
}
}
if len(lineIDToExternalID) > 0 {
outErr = errors.Join(outErr, fmt.Errorf("some lines were not found in the invoice: ids=[%s]", strings.Join(lo.Keys(lineIDToExternalID), ", ")))
}
if len(dicountIDToExternalID) > 0 {
outErr = errors.Join(outErr, fmt.Errorf("some line discounts were not found in the invoice: ids=[%s]", strings.Join(lo.Keys(dicountIDToExternalID), ", ")))
}
return outErr
}
type SyncInput interface {
models.Validator
ValidateWithInvoice(invoice StandardInvoice) error
MergeIntoInvoice(invoice *StandardInvoice) error
GetAdditionalMetadata() map[string]string
GetInvoiceID() InvoiceID
}
var _ SyncInput = (*SyncDraftStandardInvoiceInput)(nil)
type SyncDraftStandardInvoiceInput struct {
InvoiceID InvoiceID
UpsertInvoiceResults *UpsertStandardInvoiceResult
AdditionalMetadata map[string]string
InvoiceValidator func(invoice StandardInvoice) error
}
func (i SyncDraftStandardInvoiceInput) Validate() error {
var errs []error
if err := i.InvoiceID.Validate(); err != nil {
errs = append(errs, err)
}
if i.AdditionalMetadata == nil {
errs = append(errs, fmt.Errorf("additional metadata is required"))
}
return models.NewNillableGenericValidationError(errors.Join(errs...))
}
func (i SyncDraftStandardInvoiceInput) MergeIntoInvoice(invoice *StandardInvoice) error {
if invoice == nil {
return fmt.Errorf("invoice is required")
}
if i.UpsertInvoiceResults != nil {
return i.UpsertInvoiceResults.MergeIntoInvoice(invoice)
}
return nil
}
func (i SyncDraftStandardInvoiceInput) GetAdditionalMetadata() map[string]string {
return i.AdditionalMetadata
}
func (i SyncDraftStandardInvoiceInput) GetInvoiceID() InvoiceID {
return i.InvoiceID
}
func (i SyncDraftStandardInvoiceInput) ValidateWithInvoice(invoice StandardInvoice) error {
if i.InvoiceValidator != nil {
return i.InvoiceValidator(invoice)
}
return nil
}
var _ SyncInput = (*SyncIssuingStandardInvoiceInput)(nil)
type SyncIssuingStandardInvoiceInput struct {
InvoiceID InvoiceID
FinalizeInvoiceResult *FinalizeStandardInvoiceResult
AdditionalMetadata map[string]string
InvoiceValidator func(invoice StandardInvoice) error
}
func (i SyncIssuingStandardInvoiceInput) Validate() error {
var errs []error
if err := i.InvoiceID.Validate(); err != nil {
errs = append(errs, err)
}
if i.AdditionalMetadata == nil {
errs = append(errs, fmt.Errorf("additional metadata is required"))
}
return models.NewNillableGenericValidationError(errors.Join(errs...))
}
func (i SyncIssuingStandardInvoiceInput) MergeIntoInvoice(invoice *StandardInvoice) error {
if invoice == nil {
return fmt.Errorf("invoice is required")
}
if i.FinalizeInvoiceResult != nil {
return i.FinalizeInvoiceResult.MergeIntoInvoice(invoice)
}
return nil
}
func (i SyncIssuingStandardInvoiceInput) GetAdditionalMetadata() map[string]string {
return i.AdditionalMetadata
}
func (i SyncIssuingStandardInvoiceInput) GetInvoiceID() InvoiceID {
return i.InvoiceID
}
func (i SyncIssuingStandardInvoiceInput) ValidateWithInvoice(invoice StandardInvoice) error {
if i.InvoiceValidator != nil {
return i.InvoiceValidator(invoice)
}
return nil
}