Skip to content

feat: custom invocing app#2705

Merged
tothandras merged 3 commits intomainfrom
feat/custom-invoicing
Apr 23, 2025
Merged

feat: custom invocing app#2705
tothandras merged 3 commits intomainfrom
feat/custom-invoicing

Conversation

@turip
Copy link
Member

@turip turip commented Apr 22, 2025

Overview

Implement the basic backend logic for custom invoicing. Validates the hook support, plus the basic use-case of just setting the payment status.

Summary by CodeRabbit

  • New Features

    • Introduced asynchronous invoice synchronization for draft and issuing states, enabling conditional invoice state advancement based on configuration and metadata.
    • Added support for handling payment triggers within custom invoicing flows with robust validation and transactional state updates.
    • Implemented new invoicing app capabilities including invoice validation, upsert, finalization with automatic invoice number assignment, and deletion.
  • Improvements

    • Workflow apps are now always resolved in invoices, removing manual expansion flags and simplifying invoice data handling.
    • Enhanced test coverage for custom invoicing flows, including full lifecycle tests with sync hooks enabled and disabled.
    • Unified invoice merging logic and improved encapsulation of synchronization input and metadata handling.
    • Added structured validation and metadata tracking for invoice synchronization and payment trigger inputs.
    • Improved invoice state machine with logging and conditional guards for sync state advancement.
  • Bug Fixes

    • Standardized naming conventions for invoice result fields to ensure consistency across the billing system.
  • Documentation

    • Added deprecation notice for the workflow apps expansion flag in invoice APIs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 22, 2025

📝 Walkthrough

Walkthrough

This set of changes introduces asynchronous invoice synchronization capabilities and refines the expansion and state management logic in the billing and custom invoicing modules. Key updates include the addition of new interfaces and methods for syncing draft and issuing invoices, validation and merging logic for invoice results, and structured input types for sync operations. The invoice expansion mechanism is simplified by always resolving workflow apps, removing the need for explicit expansion flags. State machine transitions for invoices are now guarded by checks to determine if synchronous advancement is allowed, with logging for errors. Test suites are expanded to cover the new asynchronous flows and integration points, including full invoicing lifecycles with and without sync hooks.

Changes

Files / Areas Change Summary
api/spec/src/billing/invoices.tsp Added a deprecation comment to the workflowApps member of the InvoiceExpand enum, indicating it is deprecated as workflow apps are now always expanded.
app/common/app.go Included BillingService in the configuration when creating a new custom invoicing service factory; internal implementation update only.
openmeter/app/custominvoicing/app.go App struct now implements billing.InvoicingApp and billing.InvoicingAppAsyncSyncer. Added default invoice sequence template, metadata keys, and methods for invoice validation, upsert, finalization, deletion, and sync gating logic.
openmeter/app/custominvoicing/factory.go, openmeter/app/custominvoicing/service/service.go Extended Factory and Service structs and their configs to require and assign a billing.Service dependency; updated validation and constructors accordingly.
openmeter/app/custominvoicing/service.go Service interface now embeds new SyncService interface, which defines methods for syncing draft/issuing invoices and handling payment triggers.
openmeter/app/custominvoicing/service/sync.go Implements SyncService methods: SyncDraftInvoice, SyncIssuingInvoice, and HandlePaymentTrigger, providing validation, delegation to billing service, timestamped metadata, and transactional payment trigger handling.
openmeter/app/custominvoicing/sync.go Introduced input structs (SyncDraftInvoiceInput, SyncIssuingInvoiceInput, HandlePaymentTriggerInput) with validation methods for invoice sync and payment trigger operations.
openmeter/billing/app.go Unified casing for lineDiscountExternalIDs in UpsertResults. Added MergeIntoInvoice methods for result structs. Introduced InvoicingAppAsyncSyncer and SyncInput interfaces, and new sync input types with validation and merging logic.
openmeter/billing/httpdriver/invoice.go, openmeter/billing/invoice.go Removed WorkflowApps field from invoice expansion logic and struct; expansion of workflow apps is now always performed.
openmeter/billing/service.go Renamed ConfigIntrospectionService to ConfigService and updated interface embedding. Added SyncDraftInvoice and SyncIssuingInvoice methods to InvoiceAppService.
openmeter/billing/service/invoice.go Added internal helper for transactional invoice sync and public methods for syncing draft and issuing invoices, enforcing state preconditions.
openmeter/billing/service/invoicestate.go Added logger to InvoiceStateMachine. Introduced guards for draft/issuing sync advancement using new interface checks and logging. Unified invoice merging logic.
openmeter/server/server_test.go Added no-op implementations for SyncDraftInvoice and SyncIssuingInvoice in test billing service.
test/app/custominvoicing/invocing_test.go New test suite for custom invoicing, covering full invoicing flows with and without sync hooks, including invoice creation, syncing, payment triggers, and state assertions.
test/app/stripe/invoice_test.go Updated test to use new MergeIntoInvoice method for merging upsert results into invoices.
test/billing/invoice_test.go Consolidated and updated invoice expansion tests to expect workflow apps to always be resolved, reflecting removal of expansion flag.

Possibly related PRs

  • feat: skaffold custom invoicing #2678: Introduces initial scaffolding and API schema support for the custom invoicing app type, related as foundational work for the custom invoicing features extended in this PR.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (1.64.8)

Error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2
Failed executing command with error: you are using a configuration file for golangci-lint v2 with golangci-lint v1: please use golangci-lint v2


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea70e4a and 667b228.

📒 Files selected for processing (1)
  • app/common/app.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/common/app.go
⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: Quickstart
  • GitHub Check: E2E
  • GitHub Check: CI
  • GitHub Check: Test
  • GitHub Check: Migration Checks
  • GitHub Check: Lint
  • GitHub Check: Developer environment
  • GitHub Check: Commit hooks
  • GitHub Check: Build
  • GitHub Check: Analyze (go)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@turip turip force-pushed the feat/custom-invoicing branch 5 times, most recently from 3f889f8 to e7735e6 Compare April 22, 2025 17:52
@turip turip force-pushed the feat/custom-invoicing branch from e7735e6 to 246123a Compare April 23, 2025 09:07
@turip turip changed the title Feat/custom invoicing feat: custom invocing app Apr 23, 2025
@turip turip added release-note/feature Release note: Exciting New Features area/billing area/apps labels Apr 23, 2025
@turip turip marked this pull request as ready for review April 23, 2025 09:09
@turip turip requested a review from a team as a code owner April 23, 2025 09:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🔭 Outside diff range comments (1)
openmeter/billing/app.go (1)

214-259: 🛠️ Refactor suggestion

Discount‑ID merge is skipped when no line IDs are present & misspelled variable

lineDiscountExternalIDs are merged only when at least one lineExternalID exists, due to the outer if len(r.GetLineExternalIDs()) > 0 { … } guard.
This means discount‑IDs supplied without line‑IDs are ignored.
Additionally, the local variable is misspelled (dicountIDToExternalID).

-    // Let's merge the line IDs
-    if len(r.GetLineExternalIDs()) > 0 {
-        flattenedLines := invoice.FlattenLinesByID()
-
-        // Merge the line IDs
-        for lineID, externalID := range r.GetLineExternalIDs() {
-            ...
-        }
-
-        // Let's merge the line discount IDs
-        dicountIDToExternalID := r.GetLineDiscountExternalIDs()
-
-        for _, line := range flattenedLines {
-            ...
-        }
-    }
+    // Build a reusable flattened view
+    flattenedLines := invoice.FlattenLinesByID()
+
+    // 1. Merge line IDs (if any)
+    for lineID, externalID := range r.GetLineExternalIDs() {
+        ...
+    }
+
+    // 2. Merge discount IDs (independent from line IDs)
+    discountIDToExternalID := r.GetLineDiscountExternalIDs()
+    for _, line := range flattenedLines {
+        ...
+    }

This change:

  1. Ensures discount IDs are always merged when provided.
  2. Fixes the typo discountIDToExternalID.
🧹 Nitpick comments (9)
openmeter/app/custominvoicing/service/sync.go (4)

23-30: Avoid copy‑pasting the timestamp metadata block

SyncDraftInvoice and SyncIssuingInvoice both create a one‑element map whose sole purpose is to stamp the current time. Duplicating that logic increases the maintenance burden and risks drifting key names or the time format in the future.

-return s.billingService.SyncDraftInvoice(ctx, billing.SyncDraftInvoiceInput{
-	InvoiceID:            input.InvoiceID,
-	UpsertInvoiceResults: input.UpsertInvoiceResults,
-	AdditionalMetadata: map[string]string{
-		appcustominvoicing.MetadataKeyDraftSyncedAt: clock.Now().Format(time.RFC3339),
-	},
-})
+return s.billingService.SyncDraftInvoice(ctx, billing.SyncDraftInvoiceInput{
+	InvoiceID:            input.InvoiceID,
+	UpsertInvoiceResults: input.UpsertInvoiceResults,
+	AdditionalMetadata:   buildTimestampMetadata(appcustominvoicing.MetadataKeyDraftSyncedAt),
+})
+
+func buildTimestampMetadata(key string) map[string]string {
+	return map[string]string{key: clock.Now().Format(time.RFC3339)}
+}

Encapsulating the logic makes future changes (e.g. switching to RFC 3339Nano or injecting a clock for tests) a one‑liner.


37-44: Same timestamp helper can be reused here

SyncIssuingInvoice re‑implements the exact same pattern as above. Re‑use the helper to keep behaviour consistent and remove duplication.


51-69: Skip the extra read: reuse the invoice returned by TriggerInvoice (if available)

Inside the transaction we first call TriggerInvoice, then immediately issue a second read with GetInvoiceByID.
If TriggerInvoice already returns the updated invoice (or could be changed to do so) we could avoid:

  1. an extra database round‑trip, and
  2. the possibility of reading a stale snapshot under weaker isolation levels.

Consider extending the billing‑service interface to return the mutated invoice and remove this follow‑up query.


71-81: Minor: redundant length check before lo.Filter

lo.Filter on an empty slice is cheap and already returns an empty slice, so the explicit len(invoice.ValidationIssues) > 0 guard is unnecessary.

openmeter/app/custominvoicing/app.go (1)

118-148: Duplicate gating logic – extract a helper

CanDraftSyncAdvance and CanIssuingSyncAdvance share the same three‑step pattern: feature‑flag check ➜ nil‑metadata guard ➜ key lookup. Extracting the common part removes 15+ duplicated lines and ensures any future change (e.g. additional metadata checks) stays in sync.

test/app/custominvoicing/invocing_test.go (3)

305-307: Test description does not match the assertion

The sub‑test name says “uncollectible state”, but the code triggers billing.TriggerPaid and expects InvoiceStatusPaid.

-	s.Run("invoice can be transitioned to uncollectible state", func() {
+	s.Run("invoice can be transitioned to paid state", func() {

Keeping names in sync with behaviour prevents confusion when reading test logs.


397-416: Stale comment + sub‑test label

This flow no longer enters draft.syncing; it short‑circuits to payment_processing.pending because both hooks are disabled.

-	s.Run("invoice can be created and will end up in draft.syncing state", func() {
+	s.Run("invoice can be created and will end up in payment_processing.pending state", func() {
...
-		// We end up in payment_processing.pending state because we don't have a draft sync hook
+		// We end up in payment_processing.pending state because no draft/issuing sync hooks are enabled

113-117: Flaky tests: avoid real time.Now()

Tests rely on wall‑clock time (now := time.Now()), which can cause flakes when run near DST changes or leap seconds.
Using a fixed time (e.g. time.Date(2023, 1, 2, …, time.UTC)) or a stubbed clock keeps the suite deterministic.

openmeter/billing/app.go (1)

62-74: Minor: consider exporting lineDiscountExternalIDs or providing a copy

GetLineDiscountExternalIDs() returns the internal map directly.
Callers may inadvertently mutate the internal state.
You could either:

  1. Return a copy of the map, or
  2. Document the mutability expectation.

A simple, low‑cost fix:

func (u *UpsertResults) GetLineDiscountExternalIDs() map[string]string {
    cp := make(map[string]string, len(u.lineDiscountExternalIDs))
    for k, v := range u.lineDiscountExternalIDs {
        cp[k] = v
    }
    return cp
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b0c4fa and 246123a.

📒 Files selected for processing (19)
  • api/spec/src/billing/invoices.tsp (1 hunks)
  • app/common/app.go (1 hunks)
  • openmeter/app/custominvoicing/app.go (3 hunks)
  • openmeter/app/custominvoicing/factory.go (5 hunks)
  • openmeter/app/custominvoicing/service.go (2 hunks)
  • openmeter/app/custominvoicing/service/service.go (4 hunks)
  • openmeter/app/custominvoicing/service/sync.go (1 hunks)
  • openmeter/app/custominvoicing/sync.go (1 hunks)
  • openmeter/billing/app.go (7 hunks)
  • openmeter/billing/httpdriver/invoice.go (1 hunks)
  • openmeter/billing/invoice.go (1 hunks)
  • openmeter/billing/service.go (2 hunks)
  • openmeter/billing/service/invoice.go (2 hunks)
  • openmeter/billing/service/invoiceapp.go (2 hunks)
  • openmeter/billing/service/invoicestate.go (8 hunks)
  • openmeter/server/server_test.go (1 hunks)
  • test/app/custominvoicing/invocing_test.go (1 hunks)
  • test/app/stripe/invoice_test.go (1 hunks)
  • test/billing/invoice_test.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
app/common/app.go (2)
app/common/billing.go (1)
  • BillingService (40-89)
openmeter/app/stripe/service.go (1)
  • BillingService (44-50)
openmeter/app/custominvoicing/service/service.go (2)
openmeter/billing/service.go (1)
  • Service (9-19)
openmeter/app/service.go (1)
  • AppService (13-36)
openmeter/billing/httpdriver/invoice.go (3)
openmeter/billing/service/lineservice/service.go (1)
  • Lines (193-193)
api/api.gen.go (2)
  • InvoiceExpandLines (359-359)
  • InvoiceExpandPreceding (360-360)
api/client/go/client.gen.go (2)
  • InvoiceExpandLines (322-322)
  • InvoiceExpandPreceding (323-323)
openmeter/billing/service/invoice.go (2)
openmeter/ent/db/app.go (3)
  • Apps (339-339)
  • App (18-46)
  • App (133-150)
openmeter/billing/profile.go (1)
  • ProfileApps (261-265)
openmeter/billing/app.go (6)
openmeter/billing/invoice.go (2)
  • Invoice (329-340)
  • InvoiceID (207-207)
api/api.gen.go (1)
  • Invoice (3146-3254)
api/client/go/client.gen.go (1)
  • Invoice (2850-2958)
openmeter/app/custominvoicing/sync.go (2)
  • SyncDraftInvoiceInput (11-14)
  • SyncIssuingInvoiceInput (30-33)
pkg/models/validator.go (1)
  • Validate (16-26)
pkg/models/error.go (1)
  • NewNillableGenericValidationError (129-135)
openmeter/app/custominvoicing/sync.go (5)
openmeter/billing/invoice.go (1)
  • InvoiceID (207-207)
openmeter/billing/app.go (4)
  • UpsertInvoiceResult (76-76)
  • SyncDraftInvoiceInput (271-275)
  • FinalizeInvoiceResult (82-86)
  • SyncIssuingInvoiceInput (305-309)
pkg/models/validator.go (1)
  • Validate (16-26)
pkg/models/error.go (1)
  • NewNillableGenericValidationError (129-135)
openmeter/billing/invoicestate.go (1)
  • InvoiceTrigger (10-10)
⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: Test
  • GitHub Check: Lint
  • GitHub Check: E2E
  • GitHub Check: Developer environment
  • GitHub Check: CI
  • GitHub Check: Commit hooks
  • GitHub Check: Build
  • GitHub Check: Analyze (go)
🔇 Additional comments (46)
api/spec/src/billing/invoices.tsp (1)

415-419: Well-documented deprecation with clear rationale.

The deprecation comment clearly explains that workflow apps are now always expanded, which helps API consumers understand why they shouldn't use this enum value. This change maintains backward compatibility while moving toward a more consistent approach.

app/common/app.go (1)

128-128: Added required BillingService dependency to the custom invoicing app factory.

This change properly injects the BillingService dependency into the custom invoicing app factory, which is needed to support the new custom invoicing capabilities like invoice synchronization and workflow integration. The pattern follows existing conventions in the codebase.

test/app/stripe/invoice_test.go (1)

776-776: Improved API design using method instead of standalone function.

The code now uses the MergeIntoInvoice method on the UpsertInvoiceResult type rather than a standalone function. This change follows good object-oriented design principles by encapsulating the merge logic in the result type.

openmeter/billing/invoice.go (1)

214-218: Removed WorkflowApps field to enforce always-on expansion.

This change simplifies the invoice expansion mechanism by removing the ability to toggle workflow apps expansion. This aligns with the API changes that deprecated the corresponding enum value, making the system more consistent by always resolving workflow apps.

test/billing/invoice_test.go (2)

399-399: Test case name updated to reflect streamlined workflow app expansion

The test name was updated from "Expand scenarios - no expand" to simply "Expand scenarios", reflecting that workflow apps are now always expanded regardless of the expansion flags. This aligns with the broader change in the codebase where workflow apps are consistently resolved for all invoices.


421-424: Now asserting workflow apps are always populated

These new assertions confirm that workflow apps and their components (Tax, Invoicing, Payment) are always properly resolved, even when no explicit expansion flags are set. This is an important validation ensuring the new expected behavior works correctly.

openmeter/billing/httpdriver/invoice.go (1)

603-605: Removed WorkflowApps from expansion flags

The WorkflowApps field has been removed from the InvoiceExpand struct, aligning with the architectural decision to always expand workflow apps regardless of the provided expansion flags. This simplifies the API and ensures consistent behavior across the application.

openmeter/billing/service/invoice.go (2)

57-66: Simplified workflow apps resolution

The conditional check for invoice.ExpandedFields.WorkflowApps has been removed, making the resolveWorkflowApps method always resolve and assign apps to invoice.Workflow.Apps. This ensures workflow apps are consistently available throughout the invoice lifecycle, simplifying the codebase and reducing potential bugs from inconsistent app availability.


323-327: Now resolving workflow apps earlier in the invoice creation process

Added explicit workflow app resolution before associating invoice lines, ensuring apps are available for downstream operations like CanDraftSyncAdvance checks. This is a critical improvement that ensures custom invoicing hooks have access to the workflow apps they need to operate properly.

openmeter/server/server_test.go (2)

1443-1445: Implementation of SyncDraftInvoice in NoopBillingService looks good.

This method properly implements the billing.Service interface for testing by providing a no-op implementation of the SyncDraftInvoice method that returns an empty invoice and nil error.


1447-1449: Implementation of SyncIssuingInvoice in NoopBillingService looks good.

This method properly implements the billing.Service interface for testing by providing a no-op implementation of the SyncIssuingInvoice method that returns an empty invoice and nil error.

openmeter/app/custominvoicing/service.go (3)

7-7: Added billing package import to support the new sync functionality.

The import of the billing package is necessary to reference billing types in the new SyncService interface.


13-13: Service interface now embeds SyncService interface.

This change extends the Service interface to include sync capabilities by embedding the newly defined SyncService interface.


29-34: Added SyncService interface with invoice synchronization methods.

The new SyncService interface defines methods for:

  1. Synchronizing draft invoices
  2. Synchronizing issuing invoices
  3. Handling payment triggers

This interface extension supports the custom invoicing app's ability to interact with external systems at different stages of the invoice lifecycle.

openmeter/app/custominvoicing/service/service.go (5)

9-9: Added billing package import to support integration with billing service.

This import is required for the new billing service dependency in the Service struct.


19-20: Added billing service as a dependency to the Service struct.

The Service struct now includes the billing service as a dependency, which will be used to implement the newly introduced SyncService interface methods.


27-28: Added BillingService to the Config struct.

The Config struct now includes a BillingService field to pass the billing service dependency during service initialization.


44-46: Added validation for billing service dependency.

The Validate method now checks that the BillingService is not nil, ensuring that this required dependency is properly provided during service initialization.


57-60: Updated Service initialization to include billing service.

The New constructor now properly initializes the Service struct with the billing service from the Config.

openmeter/billing/service.go (3)

18-18: Renamed ConfigIntrospectionService to ConfigService for better clarity.

The embedded interface was renamed from ConfigIntrospectionService to ConfigService, which provides a clearer and more concise name while maintaining the same functionality.


83-85: Added async synchronization methods to InvoiceAppService interface.

Two new methods for asynchronous invoice synchronization have been added:

  1. SyncDraftInvoice - For synchronizing draft invoices
  2. SyncIssuingInvoice - For synchronizing invoices that are being issued

These methods enable integration with external systems at critical points in the invoice lifecycle.


88-88: Renamed interface to match its usage in the Service interface.

The interface name has been updated from ConfigIntrospectionService to ConfigService to maintain consistency with its reference on line 18.

openmeter/app/custominvoicing/factory.go (6)

8-8: Import of billing package added

The billing package is now correctly imported to support the integration of billing service into the custom invoicing app factory.


51-51: Added billing service dependency to Factory struct

The Factory struct now includes a billing service field, enabling invoice synchronization capabilities in the custom invoicing app.


57-57: Added billing service dependency to FactoryConfig struct

The billing service field is correctly added to the configuration struct for factory initialization.


69-71: Updated validation to require billing service

The validation now properly checks for a non-nil billing service, ensuring that the factory has all required dependencies.


84-84: Assigning billing service in factory constructor

The billing service is correctly assigned from the config to the factory instance.


109-109: Passing billing service to App instance

The billing service is now correctly passed to the App struct, enabling the app to implement the new invoicing interfaces.

openmeter/billing/service/invoiceapp.go (5)

5-5: Added fmt import

The fmt package is now imported to support error formatting in new validation methods.


63-66: Structured input type for synchronized invoice editing

The new syncEditInvoiceInput struct provides a clear interface for the internal edit operation, with fields for the sync input and expected invoice state.


68-115: Added core invoice synchronization method

The syncEditInvoice method provides transactional synchronization for invoices with proper validation, state checking, metadata merging, and state machine advancement.

Key features:

  • Validates input before proceeding
  • Ensures invoice is in the expected state
  • Merges synchronization data into the invoice
  • Updates metadata as needed
  • Advances the state machine until stable
  • Wraps errors as validation errors

This implementation correctly handles the complex workflow of synchronizing invoices with external systems.


117-122: Added method for synchronizing draft invoices

The SyncDraftInvoice method correctly delegates to the internal sync method with the appropriate expected state (InvoiceStatusDraftSyncing).


124-129: Added method for synchronizing issuing invoices

The SyncIssuingInvoice method correctly delegates to the internal sync method with the appropriate expected state (InvoiceStatusIssuingSyncing).

openmeter/app/custominvoicing/sync.go (4)

1-9: New package with imports for invoice synchronization

This file correctly establishes a new module for handling invoice synchronization with appropriate imports:

  • errors: For joining multiple validation errors
  • fmt: For error message formatting
  • billing: For invoice-related types
  • models: For validation error handling

30-47: Input structure for issuing invoice synchronization

The SyncIssuingInvoiceInput struct and validation are well implemented:

  • Appropriate fields for issuing invoice synchronization
  • Validation ensuring invoice ID is valid and finalize result is present
  • Proper error handling with joined errors and standard wrapping

Similar to the draft input, this lacks the AdditionalMetadata field found in the billing package equivalent.


49-66: Input structure for payment trigger handling

The HandlePaymentTriggerInput struct and validation are appropriate:

  • Contains necessary fields for invoice ID and trigger
  • Validates both required fields
  • Uses consistent error handling pattern with other structs

This provides a clean interface for payment triggers in the custom invoicing system.


11-28:

Details

✅ Verification successful

Input structure for draft invoice synchronization

The SyncDraftInvoiceInput struct and its validation method are well-designed:

  • Clear struct fields capturing necessary data for draft invoice sync
  • Proper validation logic checking for required fields
  • Appropriate error wrapping using models.NewNillableGenericValidationError

However, I notice that unlike the billing.SyncDraftInvoiceInput from the relevant code snippets, this struct doesn't include an AdditionalMetadata field. This might be intentional, but should be verified.


🏁 Script executed:

#!/bin/bash
# Check how the AdditionalMetadata is handled between these structs

# Look for where SyncDraftInvoiceInput is used
rg -A 10 "SyncDraftInvoiceInput" --type go

Length of output: 8400


Omitting AdditionalMetadata in the app‐level input is intentional

The SyncDraftInvoiceInput in openmeter/app/custominvoicing/sync.go only models the user‑provided fields. Any required metadata (e.g. DraftSyncedAt) is injected in openmeter/app/custominvoicing/service/sync.go when constructing the core billing.SyncDraftInvoiceInput.

No changes needed.

openmeter/billing/service/invoicestate.go (8)

7-7: Added slog import for structured logging

The slog package is now imported to support structured logging in the state machine operations.


24-24: Added Logger field to InvoiceStateMachine

The state machine now has structured logging capability, enabling better error reporting during state transitions.


113-113: Added guard conditions for draft sync advancement

The state machine now checks if draft sync can advance through the canDraftSyncAdvance method before transitioning to the next state, supporting asynchronous workflows.

Also applies to: 120-120


177-180: Added guard for issuing sync advancement

The state machine transition from issuing syncing to issued state is now conditionally guarded based on the canIssuingSyncAdvance method, properly supporting async workflows.


246-246: Logger initialization in WithInvoiceStateMachine

The state machine's logger is now properly initialized from the service's logger, ensuring consistent logging throughout the workflow.


514-514: Simplified result merging in mergeUpsertInvoiceResult

The method now directly delegates to the result's MergeIntoInvoice method, promoting consistent behavior and reducing code duplication.


567-569: Improved error handling in finalizeInvoice

The method now directly uses the result's MergeIntoInvoice method with proper error handling, making the code more consistent with other merging logic.


606-630: Implemented guards for async invoice processing

These two new methods - canDraftSyncAdvance and canIssuingSyncAdvance - are well-designed:

  • They check if the invoicing app implements the InvoicingAppAsyncSyncer interface
  • They properly query the app to determine if synchronous advancement is allowed
  • They include appropriate error handling and logging
  • They default to allowing advancement if the app doesn't implement the interface

These guards are essential for supporting asynchronous invoice workflows and are implemented in a non-breaking manner.

openmeter/app/custominvoicing/app.go (1)

80-109: FinalizeInvoice – guard against concurrent updates & lowercase draft prefix

  1. The check strings.HasPrefix(invoice.Number, "DRAFT-") is case‑sensitive; external systems might store "draft-XXX".
    Consider normalising to upper‑case before the prefix test.

  2. A concurrent finalisation may already have assigned a real invoice number between the read and the update.
    A last‑write‑wins update would silently overwrite the correct number.
    Adding an optimistic lock (e.g. WHERE version = ?) or re‑reading inside the transaction could mitigate this.

Both issues are edge‑cases but can result in duplicate invoice numbers.

tothandras
tothandras previously approved these changes Apr 23, 2025
GAlexIHU
GAlexIHU previously approved these changes Apr 23, 2025
@turip turip enabled auto-merge (squash) April 23, 2025 09:42
@tothandras tothandras disabled auto-merge April 23, 2025 11:07
@tothandras tothandras merged commit 7998007 into main Apr 23, 2025
26 of 27 checks passed
@tothandras tothandras deleted the feat/custom-invoicing branch April 23, 2025 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/billing release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants