openmeter / billing /stdinvoice.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 3)
1c4c66b verified
Raw
History Blame Contribute Delete
37.8 kB
package billing
import (
"errors"
"fmt"
"strings"
"time"
"github.com/samber/lo"
"github.com/samber/mo"
"github.com/openmeterio/openmeter/openmeter/app"
"github.com/openmeterio/openmeter/openmeter/billing/models/externalid"
"github.com/openmeterio/openmeter/openmeter/billing/models/totals"
"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/openmeter/streaming"
"github.com/openmeterio/openmeter/pkg/currencyx"
"github.com/openmeterio/openmeter/pkg/expand"
"github.com/openmeterio/openmeter/pkg/models"
"github.com/openmeterio/openmeter/pkg/pagination"
"github.com/openmeterio/openmeter/pkg/slicesx"
"github.com/openmeterio/openmeter/pkg/timeutil"
)
type StandardInvoiceStatusCategory string
const (
StandardInvoiceStatusCategoryGathering StandardInvoiceStatusCategory = "gathering"
StandardInvoiceStatusCategoryDraft StandardInvoiceStatusCategory = "draft"
StandardInvoiceStatusCategoryDelete StandardInvoiceStatusCategory = "delete"
StandardInvoiceStatusCategoryDeleted StandardInvoiceStatusCategory = "deleted"
StandardInvoiceStatusCategoryIssuing StandardInvoiceStatusCategory = "issuing"
StandardInvoiceStatusCategoryIssued StandardInvoiceStatusCategory = "issued"
StandardInvoiceStatusCategoryPaymentProcessing StandardInvoiceStatusCategory = "payment_processing"
StandardInvoiceStatusCategoryOverdue StandardInvoiceStatusCategory = "overdue"
StandardInvoiceStatusCategoryPaid StandardInvoiceStatusCategory = "paid"
StandardInvoiceStatusCategoryUncollectible StandardInvoiceStatusCategory = "uncollectible"
StandardInvoiceStatusCategoryVoided StandardInvoiceStatusCategory = "voided"
)
func (s StandardInvoiceStatusCategory) MatchesInvoiceStatus(status StandardInvoiceStatus) bool {
return status.ShortStatus() == string(s)
}
type StandardInvoiceStatus string
const (
// StandardInvoiceStatusGathering is the status of an invoice that is gathering the items to be invoiced.
StandardInvoiceStatusGathering StandardInvoiceStatus = "gathering"
StandardInvoiceStatusDraftCreated StandardInvoiceStatus = "draft.created"
// StandardInvoiceStatusDraftWaitingForCollection is the status of an invoice that is waiting for the collection to be possible (e.g. collection period has passed)
StandardInvoiceStatusDraftWaitingForCollection StandardInvoiceStatus = "draft.waiting_for_collection"
// StandardInvoiceStatusDraftCollecting is the status of an invoice that is collecting the items to be invoiced.
StandardInvoiceStatusDraftCollecting StandardInvoiceStatus = "draft.collecting"
StandardInvoiceStatusDraftUpdating StandardInvoiceStatus = "draft.updating"
StandardInvoiceStatusDraftManualApprovalNeeded StandardInvoiceStatus = "draft.manual_approval_needed"
StandardInvoiceStatusDraftValidating StandardInvoiceStatus = "draft.validating"
StandardInvoiceStatusDraftInvalid StandardInvoiceStatus = "draft.invalid"
StandardInvoiceStatusDraftSyncing StandardInvoiceStatus = "draft.syncing"
StandardInvoiceStatusDraftSyncFailed StandardInvoiceStatus = "draft.sync_failed"
StandardInvoiceStatusDraftWaitingAutoApproval StandardInvoiceStatus = "draft.waiting_auto_approval"
StandardInvoiceStatusDraftReadyToIssue StandardInvoiceStatus = "draft.ready_to_issue"
StandardInvoiceStatusDeleteInProgress StandardInvoiceStatus = "delete.in_progress"
StandardInvoiceStatusDeleteSyncing StandardInvoiceStatus = "delete.syncing"
StandardInvoiceStatusDeleteFailed StandardInvoiceStatus = "delete.failed"
StandardInvoiceStatusDeleted StandardInvoiceStatus = "deleted"
StandardInvoiceStatusIssuingSyncing StandardInvoiceStatus = "issuing.syncing"
StandardInvoiceStatusIssuingSyncFailed StandardInvoiceStatus = "issuing.failed"
StandardInvoiceStatusIssuingChargeBooking StandardInvoiceStatus = "issuing.charge_booking"
StandardInvoiceStatusIssuingChargeBookingFailed StandardInvoiceStatus = "issuing.charge_booking_failed"
StandardInvoiceStatusIssued StandardInvoiceStatus = "issued"
StandardInvoiceStatusPaymentProcessingPending StandardInvoiceStatus = "payment_processing.pending"
StandardInvoiceStatusPaymentProcessingBookingAuthorized StandardInvoiceStatus = "payment_processing.booking_authorized"
StandardInvoiceStatusPaymentProcessingBookingAuthorizedFailed StandardInvoiceStatus = "payment_processing.booking_authorized_failed"
StandardInvoiceStatusPaymentProcessingBookingAuthorizedAndSettled StandardInvoiceStatus = "payment_processing.booking_authorized_and_settled"
StandardInvoiceStatusPaymentProcessingBookingAuthorizedAndSettledFailed StandardInvoiceStatus = "payment_processing.booking_authorized_and_settled_failed"
StandardInvoiceStatusPaymentProcessingAuthorized StandardInvoiceStatus = "payment_processing.authorized"
StandardInvoiceStatusPaymentProcessingFailed StandardInvoiceStatus = "payment_processing.failed"
StandardInvoiceStatusPaymentProcessingActionRequired StandardInvoiceStatus = "payment_processing.action_required"
StandardInvoiceStatusPaymentProcessingBookingSettled StandardInvoiceStatus = "payment_processing.booking_settled"
StandardInvoiceStatusPaymentProcessingBookingSettledFailed StandardInvoiceStatus = "payment_processing.booking_settled_failed"
// These are separate statuses to allow for more gradual filtering on the API without having to understand sub-statuses
StandardInvoiceStatusOverdue StandardInvoiceStatus = "overdue"
StandardInvoiceStatusPaid StandardInvoiceStatus = "paid"
StandardInvoiceStatusUncollectible StandardInvoiceStatus = "uncollectible"
StandardInvoiceStatusVoided StandardInvoiceStatus = "voided"
)
var validStatuses = []StandardInvoiceStatus{
StandardInvoiceStatusGathering,
StandardInvoiceStatusDraftCreated,
StandardInvoiceStatusDraftWaitingForCollection,
StandardInvoiceStatusDraftCollecting,
StandardInvoiceStatusDraftUpdating,
StandardInvoiceStatusDraftManualApprovalNeeded,
StandardInvoiceStatusDraftValidating,
StandardInvoiceStatusDraftInvalid,
StandardInvoiceStatusDraftSyncing,
StandardInvoiceStatusDraftSyncFailed,
StandardInvoiceStatusDraftWaitingAutoApproval,
StandardInvoiceStatusDraftReadyToIssue,
StandardInvoiceStatusDeleteInProgress,
StandardInvoiceStatusDeleteSyncing,
StandardInvoiceStatusDeleteFailed,
StandardInvoiceStatusDeleted,
StandardInvoiceStatusIssuingSyncing,
StandardInvoiceStatusIssuingSyncFailed,
StandardInvoiceStatusIssuingChargeBooking,
StandardInvoiceStatusIssuingChargeBookingFailed,
StandardInvoiceStatusIssued,
StandardInvoiceStatusPaymentProcessingPending,
StandardInvoiceStatusPaymentProcessingBookingAuthorized,
StandardInvoiceStatusPaymentProcessingBookingAuthorizedFailed,
StandardInvoiceStatusPaymentProcessingBookingAuthorizedAndSettled,
StandardInvoiceStatusPaymentProcessingBookingAuthorizedAndSettledFailed,
StandardInvoiceStatusPaymentProcessingAuthorized,
StandardInvoiceStatusPaymentProcessingFailed,
StandardInvoiceStatusPaymentProcessingActionRequired,
StandardInvoiceStatusPaymentProcessingBookingSettled,
StandardInvoiceStatusPaymentProcessingBookingSettledFailed,
StandardInvoiceStatusOverdue,
StandardInvoiceStatusPaid,
StandardInvoiceStatusUncollectible,
StandardInvoiceStatusVoided,
}
func (s StandardInvoiceStatus) Values() []string {
return lo.Map(
validStatuses,
func(item StandardInvoiceStatus, _ int) string {
return string(item)
},
)
}
func (s StandardInvoiceStatus) ShortStatus() string {
parts := strings.SplitN(string(s), ".", 2)
return parts[0]
}
type StandardInvoiceStatusMatcher interface {
MatchesInvoiceStatus(StandardInvoiceStatus) bool
}
func (s StandardInvoiceStatus) Matches(statuses ...StandardInvoiceStatusMatcher) bool {
for _, matcher := range statuses {
if matcher.MatchesInvoiceStatus(s) {
return true
}
}
return false
}
func (s StandardInvoiceStatus) MatchesInvoiceStatus(status StandardInvoiceStatus) bool {
return s == status
}
var failedStatuses = []StandardInvoiceStatus{
StandardInvoiceStatusDraftSyncFailed,
StandardInvoiceStatusIssuingSyncFailed,
StandardInvoiceStatusIssuingChargeBookingFailed,
StandardInvoiceStatusDeleteFailed,
StandardInvoiceStatusPaymentProcessingBookingAuthorizedFailed,
StandardInvoiceStatusPaymentProcessingBookingAuthorizedAndSettledFailed,
StandardInvoiceStatusPaymentProcessingFailed,
StandardInvoiceStatusPaymentProcessingBookingSettledFailed,
}
func (s StandardInvoiceStatus) IsFailed() bool {
return lo.Contains(failedStatuses, s)
}
var finalStatuses = []StandardInvoiceStatus{
StandardInvoiceStatusDeleted,
StandardInvoiceStatusPaid,
StandardInvoiceStatusUncollectible,
StandardInvoiceStatusVoided,
}
func (s StandardInvoiceStatus) IsFinal() bool {
return lo.Contains(finalStatuses, s)
}
func (s StandardInvoiceStatus) Validate() error {
if !lo.Contains(validStatuses, s) {
return fmt.Errorf("invalid invoice status: %s", s)
}
return nil
}
// InvoiceShortStatus represents the top-level status category of an invoice
// (e.g. "draft", "issued") as exposed in the API, in contrast to
// StandardInvoiceStatus which carries the full extended value (e.g. "draft.created").
// Valid values are derived from validStatuses via ShortStatus().
type InvoiceShortStatus string
// Values returns all valid short status values except the gathering, derived from the extended statuses.
// Satisfies expand.Expandable[InvoiceShortStatus] and the FromAPIStatusFilter constraint.
func (s InvoiceShortStatus) Values() []InvoiceShortStatus {
unsupportedStatuses := []StandardInvoiceStatus{
StandardInvoiceStatusGathering,
StandardInvoiceStatusDeleteInProgress,
StandardInvoiceStatusDeleteSyncing,
StandardInvoiceStatusDeleteFailed,
StandardInvoiceStatusDeleted,
}
return lo.Uniq(lo.Map(lo.Without(validStatuses, unsupportedStatuses...), func(st StandardInvoiceStatus, _ int) InvoiceShortStatus {
return InvoiceShortStatus(st.ShortStatus())
}))
}
func (s InvoiceShortStatus) Validate() error {
if !lo.Contains(s.Values(), s) {
return fmt.Errorf("invalid invoice short status: %s", s)
}
return nil
}
type StandardInvoiceBase struct {
Namespace string `json:"namespace"`
ID string `json:"id"`
Number string `json:"number"`
Description *string `json:"description,omitempty"`
Type InvoiceType `json:"type"`
Metadata map[string]string `json:"metadata"`
Currency currencyx.Code `json:"currency,omitempty"`
Status StandardInvoiceStatus `json:"status"`
StatusDetails StandardInvoiceStatusDetails `json:"statusDetail,omitempty"`
Period *timeutil.ClosedPeriod `json:"period,omitempty"`
DueAt *time.Time `json:"dueDate,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
VoidedAt *time.Time `json:"voidedAt,omitempty"`
DraftUntil *time.Time `json:"draftUntil,omitempty"`
IssuedAt *time.Time `json:"issuedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
DeletionSource ChangeSource `json:"deletionSource,omitempty"`
SentToCustomerAt *time.Time `json:"sentToCustomerAt,omitempty"`
QuantitySnapshotedAt *time.Time `json:"quantitySnapshotedAt,omitempty"`
CollectionAt *time.Time `json:"collectionAt,omitempty"`
// PaymentProcessingEnteredAt stores when the invoice first entered payment processing
PaymentProcessingEnteredAt *time.Time `json:"paymentProcessingEnteredAt,omitempty"`
// Customer is either a snapshot of the contact information of the customer at the time of invoice being sent
// or the data from the customer entity (draft state)
// This is required so that we are not modifying the invoice after it has been sent to the customer.
Customer InvoiceCustomer `json:"customer"`
Supplier SupplierContact `json:"supplier"`
Workflow InvoiceWorkflow `json:"workflow,omitempty"`
ExternalIDs externalid.InvoiceExternalIDs `json:"externalIds,omitempty"`
SchemaLevel int `json:"schemaLevel"`
// TODO[later]: Let's also include the totals here, as that's part of the invoice db table
}
func (i StandardInvoiceBase) Validate() error {
var outErr error
if err := i.Type.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("type", err))
}
if err := i.Currency.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("currency", err))
}
if err := i.Status.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("status", err))
}
if err := i.Customer.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("customer", err))
}
if err := i.Supplier.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("supplier", err))
}
if i.Period != nil {
if err := i.Period.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("period", err))
}
}
return outErr
}
func (i StandardInvoiceBase) DefaultCollectionAtForStandardInvoice() time.Time {
if i.CollectionAt == nil {
return i.CreatedAt
}
return lo.FromPtr(i.CollectionAt)
}
func (i StandardInvoiceBase) GetDeletedAt() *time.Time {
return i.DeletedAt
}
func (i StandardInvoiceBase) GetID() string {
return i.ID
}
func (i StandardInvoiceBase) GetInvoiceID() InvoiceID {
return InvoiceID{
Namespace: i.Namespace,
ID: i.ID,
}
}
func (i StandardInvoiceBase) GetCustomerID() customer.CustomerID {
return customer.CustomerID{
Namespace: i.Namespace,
ID: i.Customer.CustomerID,
}
}
var _ GenericInvoice = (*StandardInvoice)(nil)
type StandardInvoice struct {
StandardInvoiceBase `json:",inline"`
// Entities external to the invoice itself
Lines StandardInvoiceLines `json:"lines,omitempty"`
ValidationIssues ValidationIssues `json:"validationIssues,omitempty"`
Totals totals.Totals `json:"totals"`
// private fields required by the service
ExpandedFields StandardInvoiceExpands `json:"-"`
}
func (i StandardInvoice) Validate() error {
var outErr error
if err := i.StandardInvoiceBase.Validate(); err != nil {
outErr = errors.Join(outErr, err)
}
if err := i.Lines.Validate(); err != nil {
outErr = errors.Join(outErr, ValidationWithFieldPrefix("lines", err))
}
if i.Lines.IsPresent() {
for _, line := range i.Lines.OrEmpty() {
if line.Currency != i.Currency {
outErr = errors.Join(outErr, fmt.Errorf("line[%s]: currency[%s] is not equal to invoice currency[%s]", line.ID, line.Currency, i.Currency))
}
}
}
return outErr
}
func (i StandardInvoice) CustomerID() customer.CustomerID {
return customer.CustomerID{
Namespace: i.Namespace,
ID: i.Customer.CustomerID,
}
}
func (i StandardInvoice) AsInvoice() Invoice {
return Invoice{
t: InvoiceTypeStandard,
standardInvoice: &i,
}
}
func (i StandardInvoice) GetType() InvoiceType {
return InvoiceTypeStandard
}
func (i StandardInvoice) CloneAsGenericInvoice() (GenericInvoice, error) {
cloned, err := i.Clone()
if err != nil {
return nil, err
}
return &cloned, nil
}
func (i StandardInvoice) GetGenericLines() mo.Option[[]GenericInvoiceLine] {
if !i.Lines.IsPresent() {
return mo.None[[]GenericInvoiceLine]()
}
return mo.Some(lo.Map(i.Lines.OrEmpty(), func(l *StandardLine, _ int) GenericInvoiceLine {
return &standardInvoiceLineGenericWrapper{StandardLine: l}
}))
}
func (i *StandardInvoice) SetLines(lines []GenericInvoiceLine) error {
mappedLines, err := slicesx.MapWithErr(lines, func(l GenericInvoiceLine) (*StandardLine, error) {
line, err := l.AsInvoiceLine().AsStandardLine()
if err != nil {
return nil, err
}
return &line, nil
})
if err != nil {
return fmt.Errorf("mapping lines: %w", err)
}
i.Lines = NewStandardInvoiceLines(mappedLines)
return nil
}
func (i *StandardInvoice) UnsetLines() {
i.Lines = StandardInvoiceLines{}
}
func (i *StandardInvoice) MergeValidationIssues(errIn error, reportingComponent ComponentName) error {
i.ValidationIssues = lo.Filter(i.ValidationIssues, func(issue ValidationIssue, _ int) bool {
return issue.Component != reportingComponent
})
// Regardless of the errors we need to add them to the invoice, in case the upstream service
// decides to save the invoice.
newIssues, finalErrs := ToValidationIssues(errIn)
i.ValidationIssues = append(i.ValidationIssues, newIssues...)
return finalErrs
}
func (i *StandardInvoice) HasCriticalValidationIssues() bool {
_, found := lo.Find(i.ValidationIssues, func(issue ValidationIssue) bool {
return issue.Severity == ValidationIssueSeverityCritical
})
return found
}
// RemoveMetaForCompare returns a copy of the invoice without the fields that are not relevant for higher level
// tests that compare invoices. What gets removed:
// - Line's DB state
// - Line's dependencies are marked as resolved
// - Parent pointers are removed
func (i StandardInvoice) RemoveMetaForCompare() (StandardInvoice, error) {
invoice := i
newLines, err := i.Lines.MapWithErr(func(line *StandardLine) (*StandardLine, error) {
return line.RemoveMetaForCompare()
})
if err != nil {
return StandardInvoice{}, err
}
invoice.Lines = newLines
return invoice, nil
}
// getLeafLines returns the leaf lines
func (i *StandardInvoice) getLeafLines() DetailedLines {
out := []DetailedLine{}
for _, line := range i.Lines.OrEmpty() {
// Skip non leaf nodes
out = append(out, line.DetailedLines...)
}
return out
}
type DetailedLineWithResolvedTaxConfig struct {
DetailedLine
TaxConfig *TaxConfig
}
// GetLeafLinesWithResolvedTaxConfig returns invoice leaf lines together with the effective tax
// config inherited from the parent standard line.
func (i *StandardInvoice) GetLeafLinesWithResolvedTaxConfig() []DetailedLineWithResolvedTaxConfig {
out := make([]DetailedLineWithResolvedTaxConfig, 0)
for _, parentLine := range i.Lines.OrEmpty() {
taxConfig := MergeTaxConfigs(FromProductCatalog(i.Workflow.Config.Invoicing.DefaultTaxConfig), parentLine.TaxConfig)
for _, line := range parentLine.DetailedLines {
out = append(out, DetailedLineWithResolvedTaxConfig{
DetailedLine: line,
TaxConfig: taxConfig,
})
}
}
return out
}
func (i StandardInvoice) Clone() (StandardInvoice, error) {
clone := i
clonedLines, err := i.Lines.Clone()
if err != nil {
return StandardInvoice{}, err
}
clone.Lines = clonedLines
clone.ValidationIssues = i.ValidationIssues.Clone()
clone.Totals = i.Totals
return clone, nil
}
func (i StandardInvoice) RemoveCircularReferences() (StandardInvoice, error) {
clone, err := i.Clone()
if err != nil {
return StandardInvoice{}, err
}
clone.Lines, err = clone.Lines.MapWithErr(func(line *StandardLine) (*StandardLine, error) {
return line.RemoveCircularReferences()
})
if err != nil {
return StandardInvoice{}, err
}
return clone, nil
}
func (i *StandardInvoice) SortLines() {
if !i.Lines.IsPresent() {
return
}
i.Lines.Sort()
}
type StandardInvoiceLines struct {
mo.Option[StandardLines]
}
func NewStandardInvoiceLines(children []*StandardLine) StandardInvoiceLines {
// Note: this helps with test equality checks
if len(children) == 0 {
children = nil
}
return StandardInvoiceLines{mo.Some(StandardLines(children))}
}
func (i StandardInvoiceLines) Validate() error {
return errors.Join(lo.Map(i.OrEmpty(), func(line *StandardLine, idx int) error {
return ValidationWithFieldPrefix(fmt.Sprintf("%d", idx), line.Validate())
})...)
}
func (c StandardInvoiceLines) Map(fn func(*StandardLine) *StandardLine) StandardInvoiceLines {
if !c.IsPresent() {
return c
}
return StandardInvoiceLines{
mo.Some(
c.OrEmpty().Map(fn),
),
}
}
func (c StandardInvoiceLines) MapWithErr(fn func(*StandardLine) (*StandardLine, error)) (StandardInvoiceLines, error) {
if !c.IsPresent() {
return c, nil
}
res, err := slicesx.MapWithErr(c.OrEmpty(), fn)
if err != nil {
return StandardInvoiceLines{}, err
}
return StandardInvoiceLines{mo.Some(StandardLines(res))}, nil
}
func (c StandardInvoiceLines) WithNormalizedValues() (StandardInvoiceLines, error) {
return c.MapWithErr(func(line *StandardLine) (*StandardLine, error) {
return line.WithNormalizedValues()
})
}
func (c StandardInvoiceLines) Clone() (StandardInvoiceLines, error) {
return c.MapWithErr(func(l *StandardLine) (*StandardLine, error) {
return l.Clone()
})
}
func (c StandardInvoiceLines) GetByID(id string) *StandardLine {
return lo.FindOrElse(c.Option.OrEmpty(), nil, func(line *StandardLine) bool {
return line.ID == id
})
}
func (c *StandardInvoiceLines) ReplaceByID(id string, newLine *StandardLine) bool {
if c.IsAbsent() {
return false
}
lines := c.OrEmpty()
for i, line := range lines {
if line.ID == id {
// Let's preserve the DB state of the original line (as we are only replacing the current state)
originalDBState := line.DBState
lines[i] = newLine
lines[i].DBState = originalDBState
return true
}
}
return false
}
func (c *StandardInvoiceLines) ReplaceLinesByID(lines ...*StandardLine) error {
for _, line := range lines {
if line == nil {
return fmt.Errorf("line is required")
}
if c.GetByID(line.ID) == nil {
return fmt.Errorf("replacing line[%s]: line not found", line.ID)
}
}
for _, line := range lines {
if ok := c.ReplaceByID(line.ID, line); !ok {
return fmt.Errorf("replacing line[%s]", line.ID)
}
}
return nil
}
func (c *StandardInvoiceLines) Sort() {
if c.IsAbsent() {
return
}
lines := c.OrEmpty()
lines.Sort()
c.Option = mo.Some(lines)
}
// NonDeletedLineCount returns the number of lines that are not deleted and have a valid status (e.g. we are ignoring split lines)
func (c StandardInvoiceLines) NonDeletedLineCount() int {
return lo.CountBy(c.OrEmpty(), func(l *StandardLine) bool {
return l.DeletedAt == nil
})
}
func (c *StandardInvoiceLines) Append(l ...*StandardLine) {
c.Option = mo.Some(append(c.OrEmpty(), l...))
}
func (c *StandardInvoiceLines) RemoveByID(id string) bool {
toBeRemoved := c.GetByID(id)
if toBeRemoved == nil {
return false
}
c.Option = mo.Some(
lo.Filter(c.Option.OrEmpty(), func(l *StandardLine, _ int) bool {
return l.ID != id
}),
)
return true
}
func (c StandardInvoiceLines) GetReferencedFeatureKeys() ([]string, error) {
if c.IsAbsent() {
return nil, nil
}
return c.OrEmpty().GetReferencedFeatureKeys()
}
type StandardInvoiceAvailableActions struct {
Advance *StandardInvoiceAvailableActionDetails `json:"advance,omitempty"`
Approve *StandardInvoiceAvailableActionDetails `json:"approve,omitempty"`
Delete *StandardInvoiceAvailableActionDetails `json:"delete,omitempty"`
Retry *StandardInvoiceAvailableActionDetails `json:"retry,omitempty"`
Void *StandardInvoiceAvailableActionDetails `json:"void,omitempty"`
SnapshotQuantities *StandardInvoiceAvailableActionDetails `json:"snapshotQuantities,omitempty"`
Invoice *StandardInvoiceAvailableActionInvoiceDetails `json:"invoice,omitempty"`
}
type StandardInvoiceAvailableActionDetails struct {
ResultingState StandardInvoiceStatus `json:"resultingState"`
}
type StandardInvoiceAvailableActionInvoiceDetails struct{}
type StandardInvoiceStatusDetails struct {
Immutable bool `json:"immutable"`
Failed bool `json:"failed"`
AvailableActions StandardInvoiceAvailableActions `json:"availableActions"`
}
const (
CustomerUsageAttributionTypeVersionV1 = "customer_usage_attribution.v1"
CustomerUsageAttributionTypeVersionV2 = "customer_usage_attribution.v2"
)
type (
VersionedCustomerUsageAttribution struct {
streaming.CustomerUsageAttribution `json:",inline"`
Type string `json:"type"`
}
)
type CreateInvoiceAdapterInput struct {
Namespace string
Customer customer.Customer
Profile Profile
Number string
Currency currencyx.Code
Status StandardInvoiceStatus
Metadata map[string]string
IssuedAt time.Time
Type InvoiceType
Description *string
DueAt *time.Time
CollectionAt *time.Time
Totals totals.Totals
}
func (c CreateInvoiceAdapterInput) Validate() error {
if c.Namespace == "" {
return errors.New("namespace is required")
}
if err := c.Customer.Validate(); err != nil {
return fmt.Errorf("customer: %w", err)
}
if err := c.Profile.Validate(); err != nil {
return fmt.Errorf("profile: %w", err)
}
if c.Profile.Apps == nil {
return errors.New("profile: apps must be expanded")
}
if err := c.Currency.Validate(); err != nil {
return fmt.Errorf("currency: %w", err)
}
if err := c.Status.Validate(); err != nil {
return fmt.Errorf("status: %w", err)
}
if err := c.Type.Validate(); err != nil {
return fmt.Errorf("type: %w", err)
}
if err := c.Totals.Validate(); err != nil {
return fmt.Errorf("totals: %w", err)
}
if c.Number == "" {
return errors.New("invoice number is required")
}
if c.CollectionAt != nil && c.Status != StandardInvoiceStatusGathering {
return errors.New("setting collectionAt is only allowed when creating gathering invoices")
}
return nil
}
type CreateInvoiceAdapterRespone = StandardInvoice
type AssociatedLineCountsAdapterResponse struct {
Counts map[InvoiceID]int64
}
type (
AdvanceInvoiceInput = InvoiceID
ApproveInvoiceInput = InvoiceID
ForceCollectInvoiceInput = InvoiceID
PaymentAuthorizedInput = InvoiceID
RetryInvoiceInput = InvoiceID
)
type UpdateStandardInvoiceAdapterInput = StandardInvoice
type GetInvoiceOwnershipAdapterInput = InvoiceID
type GetOwnershipAdapterResponse struct {
Namespace string
InvoiceID string
CustomerID string
}
type DeleteInvoiceInput struct {
// Invoice identifies the standard invoice to delete.
Invoice InvoiceID
// DeletionSource classifies why the invoice is being deleted.
//
// ChangeSourceAPIRequest means the delete came from the public invoice API.
// ChangeSourceSystem means the delete came from internal billing, charge, or
// subscription lifecycle code and should notify line engines about system
// standard-line deletion.
DeletionSource ChangeSource
}
func (i DeleteInvoiceInput) Validate() error {
var errs []error
if err := i.Invoice.Validate(); err != nil {
errs = append(errs, fmt.Errorf("invoice: %w", err))
}
if err := i.DeletionSource.Validate(); err != nil {
errs = append(errs, fmt.Errorf("deletion source: %w", err))
}
return errors.Join(errs...)
}
type DeleteInvoiceTriggerInput struct {
Source ChangeSource
}
func (i DeleteInvoiceTriggerInput) Validate() error {
if err := i.Source.Validate(); err != nil {
return fmt.Errorf("source: %w", err)
}
return nil
}
var _ models.Validator = DeleteInvoiceTriggerInput{}
type UpdateInvoiceLinesInternalInput struct {
Namespace string
CustomerID string
Lines []*StandardLine
}
func (i UpdateInvoiceLinesInternalInput) Validate() error {
if i.Namespace == "" {
return errors.New("namespace is required")
}
if i.CustomerID == "" {
return errors.New("customer ID is required")
}
return nil
}
// UpdateStandardInvoiceInput updates a mutable standard invoice by applying EditFn,
// diffing the edited lines against the original invoice, and letting line engines
// canonicalize line changes according to ChangeSource before persistence.
type UpdateStandardInvoiceInput struct {
// Invoice identifies the standard invoice to update.
Invoice InvoiceID
// EditFn mutates the loaded invoice into the caller's desired state.
EditFn func(*StandardInvoice) error
// ChangeSource classifies why line changes are being applied.
//
// ChangeSourceAPIRequest means the update came from the public
// invoice editing API and should be treated as a user override: invoice-owned
// lines may become manually managed, while charge-owned lines may reject the
// change.
//
// ChangeSourceSystem means the update came from internal billing,
// charge, or subscription lifecycle code. These updates should preserve system
// ownership semantics and allow engines to reconcile their own managed lines.
ChangeSource ChangeSource
// IncludeDeletedLines populates deleted lines into the invoice lines field before EditFn is called.
IncludeDeletedLines bool
}
func (i UpdateStandardInvoiceInput) Validate() error {
if err := i.Invoice.Validate(); err != nil {
return fmt.Errorf("id: %w", err)
}
if i.EditFn == nil {
return errors.New("edit function is required")
}
if err := i.ChangeSource.Validate(); err != nil {
return fmt.Errorf("line change source: %w", err)
}
return nil
}
type SimulateInvoiceInput struct {
Namespace string
CustomerID *string
Customer *customer.Customer
Number *string
Currency currencyx.Code
Lines StandardInvoiceLines
}
func (i SimulateInvoiceInput) Validate() error {
if i.CustomerID != nil {
if *i.CustomerID == "" {
return errors.New("customer ID is required")
}
}
if i.Customer != nil {
if err := i.Customer.Validate(); err != nil {
return fmt.Errorf("customer: %w", err)
}
}
if i.CustomerID == nil && i.Customer == nil {
return errors.New("either customer ID or customer is required")
}
if i.CustomerID != nil && i.Customer != nil {
return errors.New("only one of customer ID or customer can be specified")
}
if i.Namespace == "" {
return errors.New("namespace is required")
}
if i.Currency == "" {
return errors.New("currency is required")
}
if len(i.Lines.OrEmpty()) == 0 {
return errors.New("lines are required")
}
return nil
}
type UpsertValidationIssuesInput struct {
Invoice InvoiceID
Issues ValidationIssues
}
func (i UpsertValidationIssuesInput) Validate() error {
if err := i.Invoice.Validate(); err != nil {
return fmt.Errorf("id: %w", err)
}
if len(i.Issues) == 0 {
return errors.New("issues are required")
}
return nil
}
type InvoiceTriggerValidationInput struct {
// Operation specifies the operation that yielded the validation errors
// previous validation errors from this operation will be replaced by this one
Operation StandardInvoiceOperation
Errors []error
}
func (i InvoiceTriggerValidationInput) Validate() error {
if err := i.Operation.Validate(); err != nil {
return fmt.Errorf("operation: %w", err)
}
if len(i.Errors) == 0 {
return errors.New("validation errors are required")
}
return nil
}
type InvoiceTriggerInput struct {
Invoice InvoiceID
// Trigger specifies the trigger that caused the invoice to be changed, only triggerPaid and triggerPayment* are allowed
Trigger InvoiceTrigger
ValidationErrors *InvoiceTriggerValidationInput
}
func (i InvoiceTriggerInput) Validate() error {
if err := i.Invoice.Validate(); err != nil {
return fmt.Errorf("id: %w", err)
}
if i.Trigger == "" {
return errors.New("trigger is required")
}
if i.ValidationErrors != nil {
if err := i.ValidationErrors.Validate(); err != nil {
return fmt.Errorf("validation errors: %w", err)
}
}
return nil
}
type InvoiceTriggerServiceInput struct {
InvoiceTriggerInput
// AppType is the type of the app that triggered the invoice
AppType app.AppType
// Capability is the capability of the app that was processing this trigger
Capability app.CapabilityType
}
func (i InvoiceTriggerServiceInput) Validate() error {
if err := i.InvoiceTriggerInput.Validate(); err != nil {
return fmt.Errorf("trigger: %w", err)
}
if i.AppType == "" {
return errors.New("app type is required")
}
if i.Capability == "" {
return errors.New("capability is required")
}
return nil
}
type UpdateInvoiceFieldsInput struct {
Invoice InvoiceID
SentToCustomerAt mo.Option[*time.Time]
}
func (i UpdateInvoiceFieldsInput) Validate() error {
if err := i.Invoice.Validate(); err != nil {
return fmt.Errorf("id: %w", err)
}
return nil
}
type RecalculateGatheringInvoicesInput = customer.CustomerID
type StandardInvoiceExpand string
const (
StandardInvoiceExpandLines StandardInvoiceExpand = "lines"
StandardInvoiceExpandDeletedLines StandardInvoiceExpand = "deletedLines"
)
func (e StandardInvoiceExpand) Values() []StandardInvoiceExpand {
return []StandardInvoiceExpand{
StandardInvoiceExpandLines,
StandardInvoiceExpandDeletedLines,
}
}
type StandardInvoiceExpands = expand.Expand[StandardInvoiceExpand]
var StandardInvoiceExpandAll = StandardInvoiceExpands{
StandardInvoiceExpandLines,
// Deleted lines are not expanded by default
}
// InvoicePendingAdvancementFilter selects invoices that have been eligible for
// automatic advancement for at least MinimumAge as of AsOf. Scheduled states
// use their explicit due timestamp; states without one use the invoice's last
// update as the start of the pending period.
type InvoicePendingAdvancementFilter struct {
AsOf time.Time
MinimumAge time.Duration
}
var _ models.Validator = (*InvoicePendingAdvancementFilter)(nil)
func (f InvoicePendingAdvancementFilter) Validate() error {
var errs []error
if f.AsOf.IsZero() {
errs = append(errs, errors.New("as of is required"))
}
if f.MinimumAge < 0 {
errs = append(errs, errors.New("minimum age cannot be negative"))
}
return models.NewNillableGenericValidationError(errors.Join(errs...))
}
// ListStandardInvoicesPendingAdvancementInput configures retrieval of standard
// invoices that are due for automatic advancement.
type ListStandardInvoicesPendingAdvancementInput struct {
Namespaces []string
IDs []string
AsOf time.Time
MinimumAge time.Duration
}
var _ models.Validator = (*ListStandardInvoicesPendingAdvancementInput)(nil)
func (i ListStandardInvoicesPendingAdvancementInput) Validate() error {
return InvoicePendingAdvancementFilter{
AsOf: i.AsOf,
MinimumAge: i.MinimumAge,
}.Validate()
}
// CountStandardInvoicesPendingAdvancementInput configures an aggregate count
// of advancement candidates while allowing operationally disabled namespaces
// to be excluded.
type CountStandardInvoicesPendingAdvancementInput struct {
Filter InvoicePendingAdvancementFilter
ExcludedNamespaces []string
}
var _ models.Validator = (*CountStandardInvoicesPendingAdvancementInput)(nil)
func (i CountStandardInvoicesPendingAdvancementInput) Validate() error {
return i.Filter.Validate()
}
type GetStandardInvoiceByIdInput struct {
Invoice InvoiceID
Expand StandardInvoiceExpands
}
func (i GetStandardInvoiceByIdInput) Validate() error {
var errs []error
if err := i.Invoice.Validate(); err != nil {
errs = append(errs, fmt.Errorf("id: %w", err))
}
if err := i.Expand.Validate(); err != nil {
errs = append(errs, fmt.Errorf("expand: %w", err))
}
return errors.Join(errs...)
}
type ListStandardInvoicesInput struct {
pagination.Page
Namespaces []string
IDs []string
Statuses []string
ExtendedStatuses []StandardInvoiceStatus
HasAvailableAction []InvoiceAvailableActionsFilter
Expand StandardInvoiceExpands
ExternalIDs *ListInvoicesExternalIDFilter
DraftUntilLTE *time.Time
CollectionAtLTE *time.Time
IncludeDeleted bool
}
func (i ListStandardInvoicesInput) Validate() error {
var errs []error
// Page is not validated here, as for internal use we don't want to use pagination unless
// explicitly requested.
// It's the httpdriver's responsibility to validate the page size and page number.
if err := i.Expand.Validate(); err != nil {
errs = append(errs, fmt.Errorf("expand: %w", err))
}
if i.ExternalIDs != nil {
if err := i.ExternalIDs.Validate(); err != nil {
errs = append(errs, fmt.Errorf("externalIDs: %w", err))
}
}
return errors.Join(errs...)
}
type ListStandardInvoicesResponse = pagination.Result[StandardInvoice]
type CreateStandardInvoiceFromGatheringLinesInput struct {
Customer customer.CustomerID
Currency currencyx.Code
Description *string
Lines GatheringLines
PostCreationCalculationHook PostCreationCalculationHook
ForceAsyncAdvance bool
}
type (
PostCreationCalculationHook func(StandardInvoice, StandardLine) (LineMutators, error)
LineMutator func(*StandardLine) error
LineMutators = []LineMutator
)
func (i CreateStandardInvoiceFromGatheringLinesInput) Validate() error {
var errs []error
if err := i.Customer.Validate(); err != nil {
errs = append(errs, fmt.Errorf("customer: %w", err))
}
if err := i.Currency.Validate(); err != nil {
errs = append(errs, fmt.Errorf("currency: %w", err))
}
if len(i.Lines) == 0 {
errs = append(errs, fmt.Errorf("lines are required"))
}
for _, line := range i.Lines {
if err := line.Validate(); err != nil {
errs = append(errs, fmt.Errorf("line[%s]: %w", line.ID, err))
}
if line.Currency != i.Currency {
errs = append(errs, fmt.Errorf("line[%s]: currency[%s] is not equal to invoice currency[%s]", line.ID, line.Currency, i.Currency))
}
if line.Namespace != i.Customer.Namespace {
errs = append(errs, fmt.Errorf("line[%s]: namespace[%s] is not equal to invoice namespace[%s]", line.ID, line.Namespace, i.Customer.Namespace))
}
}
return errors.Join(errs...)
}
type (
StandardInvoiceHook = models.ServiceHook[StandardInvoice]
StandardInvoiceHooks = models.ServiceHookRegistry[StandardInvoice]
)
func NewSetCreditsAppliedOperation(creditsApplied CreditsApplied) LineMutator {
return func(line *StandardLine) error {
line.CreditsApplied = creditsApplied
return nil
}
}