Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Comments

feat(customer): add support for customer hierarchy#803

Merged
omkar273 merged 10 commits intoflexprice:developfrom
omkar273:feat/customer-hierarchy
Dec 2, 2025
Merged

feat(customer): add support for customer hierarchy#803
omkar273 merged 10 commits intoflexprice:developfrom
omkar273:feat/customer-hierarchy

Conversation

@omkar273
Copy link
Contributor

@omkar273 omkar273 commented Dec 1, 2025

Important

Adds customer hierarchy support and simplifies price unit model by removing price unit edges.

  • Customer Hierarchy:
    • Adds ParentCustomerID to Customer model in ent/customer.go and ent/schema/customer.go.
    • Updates CreateCustomerRequest and UpdateCustomerRequest in dto/customer.go to include parent customer fields.
    • Validates parent customer relationships in customerService in internal/service/customer.go.
  • Price Unit Simplification:
    • Removes PriceUnitEdge and related logic from ent/price.go, ent/priceunit.go, and ent/schema/price.go.
    • Deletes price unit edge queries and mutations in ent/mutation.go and ent/price_query.go.
  • Miscellaneous:
    • Updates CustomerFilter in types/customer.go to support filtering by ParentCustomerID.
    • Adds ExpandParentCustomer to CustomerExpandConfig in types/expand.go.
    • Adjusts in-memory customer store filtering in internal/testutil/inmemory_customer_store.go.

This description was created by Ellipsis for ebf4efb. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Customers can have parent‑child hierarchies (API fields, expand/filter support, domain/repo/service validation and persistence).
    • Subscriptions can specify invoice routing via a new InvoiceBilling option (invoice to parent or to self).
  • Changes

    • Price ↔ PriceUnit relationship simplified: edge-based helpers/ordering/predicates removed in favor of direct PriceUnitID handling.
    • Schema: added parent_customer_id and price_unit_id columns; corresponding FK/index adjustments.
    • API: InvoicingCustomerID made internal; invoice_billing exposed.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds nullable parent_customer_id to Customer (schema, ent, DTOs, domain, repo, service, validation, and mutations); replaces Price↔PriceUnit edge relationships with a direct nullable price_unit_id scalar and removes generated edge helpers/ordering/predicates across ent code and schema; adds invoice-billing option for subscriptions.

Changes

Cohort / File(s) Summary
Price ↔ PriceUnit edge removal
ent/client.go, ent/price.go, ent/price/price.go, ent/price/where.go, ent/price_create.go, ent/price_query.go, ent/price_update.go, ent/priceunit.go, ent/priceunit/priceunit.go, ent/priceunit/where.go, ent/priceunit_create.go, ent/priceunit_query.go, ent/priceunit_update.go, ent/schema/price.go, ent/schema/priceunit.go
Removed edge structs, edge metadata/constants, predicates, query helpers, eager-loaders, and mutation edge helpers for Price–PriceUnit and PriceUnit–Prices; eliminated sqlgraph/edge imports; replaced edge wiring with a direct nullable price_unit_id scalar in create/update/query paths and removed the previous FK reference.
Customer parent_customer_id (ent + predicates + mutations)
ent/customer.go, ent/customer/customer.go, ent/customer/where.go, ent/customer_create.go, ent/customer_update.go, ent/mutation.go, ent/schema/customer.go
Added nullable parent_customer_id column and Customer field; generated predicates (EQ/NEQ/IN/GT/LTE/Contains/Prefix/Suffix/IsNil/NotNil/EqualFold/ContainsFold), ordering helper, create/update setters/clearers, mutation accessors, and scan/assign/String handling.
Schema / migrations
ent/migrate/schema.go
Added parent_customer_id to customers and price_unit_id to prices (nullable varchar(50)); adjusted Prices table index column positions and removed the prior PriceUnit FK reference.
API DTOs & domain model
internal/api/dto/customer.go, internal/domain/customer/model.go, internal/types/customer.go, internal/types/expand.go
DTOs: added ParentCustomerID/ParentCustomerExternalID on create/update with mutual-exclusion validation and added ParentCustomer to response; domain: added ParentCustomerID mapping; types: added ParentCustomerIDs filter and ExpandParentCustomer config.
Repository & service changes
internal/repository/ent/customer.go, internal/service/customer.go
Repository persists and filters by ParentCustomerID; service resolves/validates parent by internal/external ID, enforces hierarchy rules (no self-parent, no nested parents, no reassign when active subscriptions, no assigning children as child), and supports expand/batched parent loading.
Subscription invoicing & related DTOs/tests
internal/service/subscription.go, internal/testutil/inmemory_customer_store.go, internal/api/dto/subscription.go, internal/types/subscription.go
Added InvoiceBilling type/validation (invoice_to_parent / invoice_to_self); CreateSubscription uses parent customer as invoicing target when configured; DTO exposure of InvoicingCustomerID made internal; in-memory test store and filters updated for parent IDs.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant API as API handler
participant Svc as Customer Service
participant Repo as Ent Repository
participant DB as Database
Note over API,Svc: CreateCustomer / UpdateCustomer flows with parent resolution
API->>Svc: Submit Create/Update (may include ParentCustomerID or ParentCustomerExternalID, InvoiceBilling)
Svc->>Repo: Resolve parent by ExternalID or ID (if provided)
Repo->>DB: Query customer by external_id / id
DB-->>Repo: Return parent record or not found
Repo-->>Svc: Parent resolved (ID) or error
Svc->>Svc: validateParentCustomerAssignment (check subscriptions/children/cycles)
Svc->>Repo: Create/Update ent Customer with SetNillableParentCustomerID(parentID)
Repo->>DB: Insert/Update customer row (parent_customer_id set/cleared)
DB-->>Repo: OK
Repo-->>Svc: Persisted customer
Svc-->>API: Return created/updated customer (with optional expanded ParentCustomer)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • ent/migrate/schema.go — index column reindexing and FK removal for migration correctness.
    • ent/* changes removing Price/PriceUnit edges — ensure no remaining edge references, imports, or ordering helpers remain and that price_unit_id handling is consistent.
    • internal/service/customer.go — validateParentCustomerAssignment: correctness around active-subscription checks, cycle prevention, and concurrent-change considerations.
    • Batch parent expansion in GetCustomers — verify batching avoids N+1 and handles recursion/limits.
    • Subscription invoicing changes — ensure InvoiceBilling defaults/validation and invoicing target resolution are correct.

Suggested labels

enhancement, need-db-migration

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows the required format with type 'feat', module 'customer', and a clear, actionable message about adding customer hierarchy support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 074b1fd and a02b654.

📒 Files selected for processing (1)
  • internal/service/subscription.go (1 hunks)

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

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

Important

Looks good to me! 👍

Reviewed everything up to ebf4efb in 2 minutes and 2 seconds. Click for details.
  • Reviewed 2582 lines of code in 31 files
  • Skipped 0 files when reviewing.
  • Skipped posting 2 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. internal/testutil/inmemory_customer_store.go:30
  • Draft comment:
    The deep copy function ‘copyCustomer’ is missing the new ParentCustomerID field. Without copying c.ParentCustomerID, any customer hierarchy data will be lost in the in‐memory store.
  • Reason this comment was not posted:
    Comment was not on a location in the diff, so it can't be submitted as a review comment.
2. ent/schema/customer.go:82
  • Draft comment:
    Consider adding an index on the 'parent_customer_id' field if you plan to query customers by parent. This can improve query performance for hierarchy lookups.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 15% vs. threshold = 50% This comment is suggesting a performance optimization (adding an index). However, it's speculative - it says "if you plan to query customers by parent" which is conditional. The comment doesn't know for certain whether this field will be queried in a way that requires an index. According to the rules, I should NOT make speculative comments like "If X, then Y is an issue" - I should only comment if it's definitely an issue. This comment is conditional on future usage patterns that aren't evident from the diff itself. Additionally, whether to add an index is a performance consideration that depends on query patterns, data volume, and other factors not visible in this change. The comment could be valid if there's clear evidence that parent_customer_id will be used in queries (like if there were edges or relationships defined), but I don't see that in the current file. The field might just be for data storage without query requirements. While there could be implicit usage patterns, the comment is explicitly conditional ("if you plan to query") which makes it speculative. Without seeing actual query code or relationship definitions that would require this index, this is a "nice to have" suggestion rather than a clear code issue. This comment should be deleted because it's speculative (conditional on "if you plan to query") and doesn't point to a definite issue. It's a performance suggestion that may or may not be needed depending on usage patterns not evident in the diff.

Workflow ID: wflow_oVI9SIYWyxFxMmar

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

Copy link

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/testutil/inmemory_customer_store.go (1)

26-56: In-memory store drops ParentCustomerID, breaking new filter behavior

customerFilterFn now filters on f.ParentCustomerIDs, but copyCustomer doesn’t copy ParentCustomerID, so customers stored in-memory always have ParentCustomerID == nil. This makes parent‑based filters always fail in tests and diverges from real repository behavior.

Recommend updating copyCustomer to include the field:

 func copyCustomer(c *customer.Customer) *customer.Customer {
@@
-	c = &customer.Customer{
-		ID:                c.ID,
-		ExternalID:        c.ExternalID,
-		Name:              c.Name,
-		Email:             c.Email,
+	c = &customer.Customer{
+		ID:               c.ID,
+		ExternalID:       c.ExternalID,
+		Name:             c.Name,
+		Email:            c.Email,
+		ParentCustomerID: c.ParentCustomerID,
@@
-		EnvironmentID:     c.EnvironmentID,
+		EnvironmentID: c.EnvironmentID,

Also applies to: 154-162

internal/api/dto/customer.go (1)

134-136: Bug: Incorrect index-to-string conversion.

string(rune(i)) converts the integer to a Unicode code point, not a decimal string. For i=0, this produces an empty/null character; for i=1, it produces a control character. This results in confusing error messages.

-				return ierr.WithError(err).
-					WithHint("Invalid tax rate configuration at index " + string(rune(i))).
+				return ierr.WithError(err).
+					WithHint("Invalid tax rate configuration at index " + strconv.Itoa(i)).
					Mark(ierr.ErrValidation)
-				return ierr.WithError(err).
-					WithHint("Invalid integration entity mapping at index " + string(rune(i))).
+				return ierr.WithError(err).
+					WithHint("Invalid integration entity mapping at index " + strconv.Itoa(i)).
					Mark(ierr.ErrValidation)

You'll need to add "strconv" to the imports.

Also applies to: 144-147

🧹 Nitpick comments (11)
internal/types/expand.go (1)

137-143: Confirm intended scope for CustomerExpandConfig / nested customer expands

CustomerExpandConfig correctly allows parent_customer with no nested expands. If you intend to support expressions like customer.parent_customer when expanding from subscriptions, invoices, or alert logs, you’ll also need to add ExpandParentCustomer into the relevant NestedExpands for ExpandCustomer in those configs; otherwise this remains limited to top-level customer endpoints.

ent/schema/priceunit.go (1)

75-77: Edge removal from PriceUnit is coherent with scalar PriceUnitID approach

Returning nil from Edges() cleanly removes graph edges from PriceUnit. This matches the move away from WithPrices/edge traversal. Just ensure your DB migrations drop or adjust any previous FK/constraints associated with this edge so schema and generated code stay in sync.

ent/customer/customer.go (1)

50-52: parent_customer_id wiring and ordering helper look correct

Adding FieldParentCustomerID, including it in Columns, and exposing ByParentCustomerID is consistent with the rest of the generated customer API. As hierarchies grow, you may want to ensure there’s an appropriate DB index on parent_customer_id if it becomes a common filter/sort key, but functionally this looks good.

Also applies to: 76-77, 196-199

internal/repository/ent/customer.go (1)

59-79: ParentCustomerID create/update wiring is consistent; double‑check clear semantics

Using SetNillableParentCustomerID(c.ParentCustomerID) on both create and update correctly propagates the pointer field through to Ent. One thing to confirm: on updates, a nil here means “do not touch DB value”, not “clear to NULL” — clearing would require an explicit ClearParentCustomerID() call. If your update DTO / domain model needs to support removing a parent (as distinct from “no change”), you’ll need an explicit signal and a corresponding branch in the update builder.

Also applies to: 322-341

internal/types/customer.go (1)

16-23: CustomerFilter extensions look consistent; consider validating ID slices

The added filter fields (Filters, Sort, ID slices, and ParentCustomerIDs) are wired consistently with existing patterns (tags, omitempty, etc.). If you expect untrusted input here, consider optionally reusing ValidateCustomerID / validateID for CustomerIDs, ExternalIDs, and ParentCustomerIDs to fail fast on obviously bad identifiers, but it’s not required for correctness.

internal/service/subscription.go (1)

82-97: Invoicing customer restriction to self/parent is correctly enforced

The new validation cleanly restricts InvoicingCustomerID to either the customer itself or its direct parent, and returns a clear, reportable validation error otherwise. This is consistent with the described hierarchy semantics and still reuses the existing “invoicing customer must be active” check.

ent/schema/customer.go (1)

82-88: parent_customer_id field matches domain usage; consider indexing for parent lookups

The parent_customer_id field definition (varchar(50), Nillable().Optional()) is aligned with the Ent and domain model changes and supports nullable parent relationships.

If you expect to query by ParentCustomerID regularly (e.g., via ParentCustomerIDIn filters), consider adding an index on (tenant_id, environment_id, parent_customer_id) here to keep those lookups performant at scale.

internal/service/customer.go (2)

35-58: Parent customer validation logic is correct.

The validation correctly prevents nested hierarchies by ensuring a parent customer cannot itself be a child. The dual lookup paths (external ID vs internal ID) are handled appropriately, with normalization to internal ID for downstream processing.

Consider extracting the duplicate "parent cannot be a child" validation into a small helper to reduce repetition, but this is optional.


206-216: Consider adding recursion depth limit for defensive coding.

The recursive GetCustomer call works correctly under normal conditions since the validation logic prevents nested hierarchies. However, if data becomes corrupted (e.g., manual DB edits), this could cause infinite recursion or a stack overflow.

Since the current design only allows single-level parent-child relationships, this is low risk, but consider adding a simple depth check for resilience:

-func (s *customerService) GetCustomer(ctx context.Context, id string) (*dto.CustomerResponse, error) {
+func (s *customerService) GetCustomer(ctx context.Context, id string) (*dto.CustomerResponse, error) {
+	return s.getCustomerWithDepth(ctx, id, 0)
+}
+
+func (s *customerService) getCustomerWithDepth(ctx context.Context, id string, depth int) (*dto.CustomerResponse, error) {
+	if depth > 1 { // Only single-level hierarchy allowed
+		return nil, ierr.NewError("parent hierarchy depth exceeded").Mark(ierr.ErrInvalidOperation)
+	}
internal/api/dto/customer.go (1)

104-112: Comment on line 110 may be misleading.

The comment states "If you provide the external ID, the parent customer value will be ignored" which could confuse API consumers. Since validation enforces mutual exclusivity (only one can be provided), this phrasing is inaccurate—both cannot be sent together.

Consider clarifying the comment:

-	// If you provide the external ID, the parent customer value will be ignored
+	// Note: Only one of parent_customer_id or parent_customer_external_id may be provided
ent/migrate/schema.go (1)

673-673: Consider: No foreign key constraint on parent_customer_id.

The parent_customer_id column is added without a foreign key constraint referencing the customers table. This allows orphaned references if a parent customer is deleted. Depending on your requirements, you may want to add referential integrity at the database level.

If referential integrity is desired, consider adding an FK in the Ent schema:

// In ent/schema/customer.go
func (Customer) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("parent", Customer.Type).
            Unique().
            Field("parent_customer_id"),
    }
}

Alternatively, soft-delete patterns and service-layer validation (which appears to be implemented) may be sufficient for your use case.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between afe7ff2 and ebf4efb.

📒 Files selected for processing (31)
  • ent/client.go (0 hunks)
  • ent/customer.go (4 hunks)
  • ent/customer/customer.go (3 hunks)
  • ent/customer/where.go (2 hunks)
  • ent/customer_create.go (2 hunks)
  • ent/customer_update.go (4 hunks)
  • ent/migrate/schema.go (4 hunks)
  • ent/mutation.go (18 hunks)
  • ent/price.go (1 hunks)
  • ent/price/price.go (0 hunks)
  • ent/price/where.go (0 hunks)
  • ent/price_create.go (1 hunks)
  • ent/price_query.go (3 hunks)
  • ent/price_update.go (2 hunks)
  • ent/priceunit.go (1 hunks)
  • ent/priceunit/priceunit.go (0 hunks)
  • ent/priceunit/where.go (0 hunks)
  • ent/priceunit_create.go (0 hunks)
  • ent/priceunit_query.go (1 hunks)
  • ent/priceunit_update.go (0 hunks)
  • ent/schema/customer.go (1 hunks)
  • ent/schema/price.go (1 hunks)
  • ent/schema/priceunit.go (1 hunks)
  • internal/api/dto/customer.go (4 hunks)
  • internal/domain/customer/model.go (2 hunks)
  • internal/repository/ent/customer.go (3 hunks)
  • internal/service/customer.go (7 hunks)
  • internal/service/subscription.go (1 hunks)
  • internal/testutil/inmemory_customer_store.go (1 hunks)
  • internal/types/customer.go (1 hunks)
  • internal/types/expand.go (2 hunks)
💤 Files with no reviewable changes (7)
  • ent/price/where.go
  • ent/price/price.go
  • ent/priceunit_create.go
  • ent/priceunit_update.go
  • ent/priceunit/priceunit.go
  • ent/priceunit/where.go
  • ent/client.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-18T10:22:48.348Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/billing.go:1454-1477
Timestamp: 2025-10-18T10:22:48.348Z
Learning: In `internal/service/billing.go`, the `GetCustomerEntitlements` function passes `subscriptions[0].ID` to `AggregateEntitlements` as a fallback subscription ID. This is acceptable because customers typically have a single subscription, and the subscription ID parameter is only used as fallback metadata in `EntitlementSource` objects, not for business logic or filtering.

Applied to files:

  • internal/service/subscription.go
🧬 Code graph analysis (18)
ent/customer_create.go (2)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
ent/customer/customer.go (1)
  • FieldParentCustomerID (51-51)
internal/types/customer.go (2)
internal/types/search_filter.go (2)
  • FilterCondition (55-60)
  • SortCondition (138-141)
ent/customer/where.go (2)
  • ExternalID (103-105)
  • Email (113-115)
ent/customer/customer.go (2)
ent/subscription/subscription.go (1)
  • OrderOption (330-330)
ent/plan/plan.go (1)
  • OrderOption (101-101)
ent/customer.go (2)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
ent/customer/customer.go (1)
  • FieldParentCustomerID (51-51)
ent/priceunit_query.go (7)
ent/priceunit.go (2)
  • PriceUnit (17-48)
  • PriceUnit (51-68)
ent/schema/priceunit.go (6)
  • PriceUnit (14-16)
  • PriceUnit (19-23)
  • PriceUnit (26-31)
  • PriceUnit (34-72)
  • PriceUnit (75-77)
  • PriceUnit (80-87)
ent/price/where.go (1)
  • PriceUnit (128-130)
ent/predicate/predicate.go (1)
  • PriceUnit (94-94)
ent/subscriptionlineitem/where.go (1)
  • PriceUnit (155-157)
ent/invoicelineitem/where.go (1)
  • PriceUnit (160-162)
internal/domain/priceunit/model.go (1)
  • PriceUnit (11-21)
internal/api/dto/customer.go (5)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
internal/errors/builder.go (1)
  • NewError (17-19)
internal/errors/errors.go (1)
  • ErrValidation (16-16)
internal/types/context.go (1)
  • GetEnvironmentID (54-59)
internal/validator/validator.go (1)
  • ValidateRequest (35-52)
ent/customer/where.go (3)
ent/customer.go (2)
  • Customer (17-58)
  • Customer (61-76)
ent/schema/customer.go (5)
  • Customer (14-16)
  • Customer (19-25)
  • Customer (28-89)
  • Customer (92-94)
  • Customer (97-109)
ent/customer/customer.go (1)
  • FieldParentCustomerID (51-51)
internal/repository/ent/customer.go (1)
ent/customer/where.go (2)
  • ParentCustomerID (148-150)
  • ParentCustomerIDIn (1263-1265)
ent/price_create.go (6)
ent/price/where.go (1)
  • PriceUnitID (123-125)
ent/subscriptionlineitem/where.go (1)
  • PriceUnitID (150-152)
ent/invoicelineitem/where.go (1)
  • PriceUnitID (155-157)
ent/price/price.go (1)
  • FieldPriceUnitID (39-39)
ent/subscriptionlineitem/subscriptionlineitem.go (1)
  • FieldPriceUnitID (51-51)
ent/invoicelineitem/invoicelineitem.go (1)
  • FieldPriceUnitID (53-53)
internal/service/customer.go (7)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
internal/errors/builder.go (1)
  • NewError (17-19)
internal/api/dto/customer.go (1)
  • CustomerResponse (116-119)
ent/predicate/predicate.go (1)
  • Customer (52-52)
internal/types/expand.go (2)
  • CustomerExpandConfig (138-143)
  • ExpandParentCustomer (38-38)
internal/types/customer.go (2)
  • NewNoLimitCustomerFilter (33-37)
  • NewCustomerFilter (26-30)
internal/types/subscription.go (1)
  • NewSubscriptionFilter (214-218)
ent/priceunit.go (1)
ent/priceunit/where.go (1)
  • Precision (129-131)
internal/testutil/inmemory_customer_store.go (1)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
ent/price_update.go (6)
ent/price/where.go (1)
  • PriceUnitID (123-125)
ent/subscriptionlineitem/where.go (1)
  • PriceUnitID (150-152)
ent/invoicelineitem/where.go (1)
  • PriceUnitID (155-157)
ent/price/price.go (1)
  • FieldPriceUnitID (39-39)
ent/subscriptionlineitem/subscriptionlineitem.go (1)
  • FieldPriceUnitID (51-51)
ent/invoicelineitem/invoicelineitem.go (1)
  • FieldPriceUnitID (53-53)
ent/mutation.go (4)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
ent/customer/customer.go (1)
  • FieldParentCustomerID (51-51)
ent/price/where.go (1)
  • PriceUnitID (123-125)
ent/ent.go (1)
  • Value (65-65)
internal/domain/customer/model.go (1)
ent/customer/where.go (1)
  • ParentCustomerID (148-150)
ent/price_query.go (4)
ent/ent.go (2)
  • QueryContext (67-67)
  • Interceptor (70-70)
ent/price/price.go (1)
  • OrderOption (189-189)
internal/domain/price/model.go (1)
  • Price (31-127)
ent/predicate/predicate.go (1)
  • Price (91-91)
ent/migrate/schema.go (2)
ent/customer/customer.go (1)
  • Columns (57-77)
ent/price/price.go (1)
  • Columns (97-137)
ent/price.go (1)
ent/price/where.go (1)
  • GroupID (228-230)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests and Lint
🔇 Additional comments (26)
internal/types/expand.go (1)

38-38: New ExpandParentCustomer field looks consistent

The new ExpandParentCustomer constant aligns with existing ExpandableField usage and naming; no issues from a types/validation perspective.

ent/priceunit_query.go (1)

333-337: sqlAll simplification is correct; note loss of eager-loaded Prices

The updated sqlAll that just builds _spec and appends into a plain nodes slice is functionally sound. With the PriceUnit→Prices edge removed, it’s expected that no edge-loading happens here anymore, so callers must now join/filter by PriceUnitID on the price side rather than relying on WithPrices.

ent/mutation.go (4)

19209-19256: Generated mutation methods follow standard Ent patterns.

These parent_customer_id mutation methods correctly implement the standard Ent pattern for nullable string fields:

  • SetParentCustomerID / ParentCustomerID for value access
  • OldParentCustomerID for retrieving previous value during updates
  • Clear / Cleared / Reset for nullable field lifecycle management

The implementation is consistent with other nullable fields in this file (e.g., address_country).


39544-39588: Edge removal for PriceMutation is consistent with FK column conversion.

The removal of Price-PriceUnit edge handling aligns with the AI summary indicating conversion from edge relationships to direct foreign-key column handling. The price_unit_id is now treated as a plain field rather than an edge relationship.

This simplifies the data model by using direct FK columns instead of Ent edge abstractions.


40620-40664: PriceUnitMutation edge removal mirrors PriceMutation changes.

The edge handling removal on PriceUnitMutation is the inverse side of the Price-PriceUnit relationship conversion. Both entities now operate without edge abstractions for this relationship.


18343-18343: Verify generated code matches schema definition.

This file is auto-generated by Ent from schema definitions. Ensure the parent_customer_id field is properly defined in the schema (likely ent/schema/customer.go) with appropriate constraints such as:

  • Optional/nullable designation
  • Foreign key constraint to the customers table (self-referential)
  • Index for query performance on hierarchical lookups
internal/repository/ent/customer.go (1)

495-507: ParentCustomerIDs filter integrates cleanly with Ent predicates

The new ParentCustomerIDs handling (len(f.ParentCustomerIDs) > 0customer.ParentCustomerIDIn(...)) is consistent with the Ent where-predicate surface and matches the in‑memory store’s behavior.

ent/schema/price.go (1)

216-219: Edges removal aligns with scalar price_unit_id migration

Having Edges() return nil here is consistent with removing the PriceUnit edge in favor of the scalar price_unit_id field. As long as edge-based queries have been updated to use the scalar ID (which this PR appears to do), this looks good.

internal/domain/customer/model.go (1)

23-25: Domain-level ParentCustomerID plumbed correctly from Ent

Adding ParentCustomerID *string to the domain Customer and mapping it in FromEnt keeps the domain model in sync with the Ent schema and repository usage (e.g., subscription invoicing validation). This also matches the DB column name via tags.

Also applies to: 59-65

ent/price.go (1)

97-99: GroupID nullable handling is consistent with nillable schema

Switching GroupID to *string plus the updated scan/assign and String() handling correctly aligns this model with a nullable group_id column. Callers will now see null/omitted JSON for unset group IDs instead of an empty string, which is typically more accurate for optional fields.

Also applies to: 112-113, 377-383, 547-550

ent/priceunit.go (1)

46-48: LGTM - Generated code reflects schema changes.

This is auto-generated ent code. The formatting change to the Precision field and removal of edge-related code aligns with the broader refactoring to use direct foreign-key columns instead of edge traversal.

ent/customer_create.go (2)

229-243: LGTM - Generated setter methods follow ent conventions.

The SetParentCustomerID and SetNillableParentCustomerID methods are correctly generated, following the same pattern as other nullable string fields in this file.


440-443: LGTM - createSpec correctly handles the new field.

The ParentCustomerID is properly serialized to the create spec and assigned as a pointer to _node.ParentCustomerID, consistent with how other optional ID fields (like MeterID, EntityID) are handled elsewhere.

ent/customer.go (3)

55-57: LGTM - ParentCustomerID field correctly defined.

The nullable ParentCustomerID field is properly typed as *string with appropriate JSON tags, consistent with how other nullable reference fields would be defined.


196-202: LGTM - Correct nullable string scanning pattern.

The assignment correctly allocates a new string pointer and assigns the value when Valid is true, matching the pattern used for other pointer fields in ent-generated code.


289-293: LGTM - String() method correctly handles nil ParentCustomerID.

The nil check before dereferencing ensures safe string representation when ParentCustomerID is not set.

ent/price_create.go (1)

695-698: LGTM - Direct field handling replaces edge-based relationship.

The PriceUnitID is now handled as a direct field rather than through edge traversal. This correctly assigns the value as a pointer to _node.PriceUnitID, consistent with the broader refactoring to use direct foreign-key columns.

internal/service/customer.go (3)

254-301: LGTM - Efficient batch fetching for parent customer expansion.

The implementation correctly:

  1. Validates expand fields early
  2. Returns early for empty results
  3. Batches parent customer fetches to avoid N+1 queries
  4. Uses a map for O(1) lookups when attaching parents

The nil map access on line 297 (parentCustomersByID[*resp.Customer.ParentCustomerID]) is safe in Go when using the comma-ok idiom, as accessing a nil map returns the zero value and false.


342-361: LGTM - Proper handling of parent customer assignment updates.

The implementation correctly:

  1. Trims whitespace for robust comparison
  2. Only runs expensive validations when the hierarchy actually changes
  3. Distinguishes between clearing the parent (empty string → nil) and setting a new parent

579-633: Well-designed validation for parent-child hierarchy constraints.

The validateParentCustomerAssignment method comprehensively enforces business rules:

  1. Active subscriptions check (lines 580-594): Prevents hierarchy changes that could complicate billing
  2. Self-reference guard (lines 601-605): Prevents circular self-assignment
  3. Nested hierarchy prevention (lines 607-615): Ensures only single-level parent-child relationships
  4. Parent-with-children guard (lines 617-630): Prevents a customer that has children from becoming a child itself

The use of Limit = 1 for existence checks is efficient. Error messages with actionable hints provide good UX.

internal/api/dto/customer.go (4)

60-67: LGTM - Parent customer ID fields added correctly.

The mutual exclusivity between parent_customer_id and parent_customer_external_id is documented clearly and validated in the Validate() method.


152-157: Validation logic is correct.

The mutual exclusivity check properly prevents both ParentCustomerID and ParentCustomerExternalID from being provided simultaneously.


181-194: LGTM - UpdateCustomerRequest validation properly implemented.

Follows the same validation pattern as CreateCustomerRequest with correct error handling.


175-175: Verify: ParentCustomerExternalID resolution happens in service layer.

ToCustomer() only maps ParentCustomerID directly. Ensure the service layer resolves ParentCustomerExternalID to ParentCustomerID before persisting.

ent/customer/where.go (2)

147-150: LGTM: ParentCustomerID alias is consistent with existing helpers

The public ParentCustomerID convenience alias cleanly mirrors patterns like TenantID and EnvironmentID, correctly delegating to FieldEQ(FieldParentCustomerID, v) and aligning with the comment about being identical to ParentCustomerIDEQ.


1252-1325: LGTM: Full predicate set for parent_customer_id matches other string fields

The ParentCustomerID* predicates (EQ/NEQ, In/NotIn, GT/GTE/LT/LTE, Contains/HasPrefix/HasSuffix, IsNil/NotNil, EqualFold/ContainsFold) are complete and follow the same naming and implementation conventions as EnvironmentID* / ExternalID*, with consistent use of FieldParentCustomerID.

…g customer logic and enhance validation for subscription creation
… and update related logic for improved clarity and consistency
Copy link

@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: 0

🧹 Nitpick comments (1)
internal/types/subscription.go (1)

25-40: Validation logic is correct and consistent with existing patterns.

The Validate() method properly checks against allowed values and returns a well-structured error. The implementation is consistent with other validators in this file.

Optional: Consider a more descriptive hint message.

Line 32's hint "Invalid invoice billing" duplicates the main error message. A more descriptive hint like "Invoice billing must be 'invoice_to_parent' or 'invoice_to_self'" would provide clearer guidance (similar to how SubscriptionChangeType.Validate() does on line 374).

However, this follows the existing pattern for most validators in this file, so it's not inconsistent with the codebase.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 662ac07 and 074b1fd.

📒 Files selected for processing (3)
  • internal/api/dto/subscription.go (2 hunks)
  • internal/service/subscription.go (1 hunks)
  • internal/types/subscription.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/api/dto/subscription.go
  • internal/service/subscription.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-18T10:22:48.348Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/billing.go:1454-1477
Timestamp: 2025-10-18T10:22:48.348Z
Learning: In `internal/service/billing.go`, the `GetCustomerEntitlements` function passes `subscriptions[0].ID` to `AggregateEntitlements` as a fallback subscription ID. This is acceptable because customers typically have a single subscription, and the subscription ID parameter is only used as fallback metadata in `EntitlementSource` objects, not for business logic or filtering.

Applied to files:

  • internal/types/subscription.go
📚 Learning: 2025-10-17T21:53:42.505Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/subscription.go:3495-3534
Timestamp: 2025-10-17T21:53:42.505Z
Learning: In `internal/service/subscription.go`, when adding multiple-type addons to a subscription, existing active line items are intentionally reused rather than creating new line items for each addon association. The addon occurrences are tracked via separate `AddonAssociation` records, but billing happens through the shared `SubscriptionLineItem`. This is the expected behavior and should not be flagged as an issue.

Applied to files:

  • internal/types/subscription.go
🧬 Code graph analysis (1)
internal/types/subscription.go (3)
internal/domain/entityintegrationmapping/model.go (1)
  • Validate (84-141)
internal/errors/builder.go (1)
  • NewError (17-19)
internal/errors/errors.go (1)
  • ErrValidation (16-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests and Lint
🔇 Additional comments (2)
internal/types/subscription.go (2)

10-19: LGTM! Clean type definition with clear constants.

The InvoiceBilling type is well-defined with descriptive comments and follows Go conventions. The two constants clearly represent the parent vs. self invoicing options needed for the customer hierarchy feature.


21-23: LGTM! Standard String() implementation.

The method correctly converts the type to string, consistent with other types in this file.

return &dto.CustomerResponse{Customer: customer}, nil
resp := &dto.CustomerResponse{Customer: customer}

if customer.ParentCustomerID != nil {
Copy link
Member

Choose a reason for hiding this comment

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

ideally this should go only if requested explicitly in expand
this will lead to two set of queries but okay.
Can move this in a follow up PR behind expand param and handle in function


// Only run validations if the hierarchy is changing
if newParentID != currentParentID {
if err := s.validateParentCustomerAssignment(ctx, cust, newParentID); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

follow up : maintain these changes in an audit log system

// Handle InvoiceBilling to set InvoicingCustomerID internally
// The DTO layer ensures InvoiceBilling is always set (defaults to invoice_to_self)
// For invoice_to_self, we don't need to set InvoicingCustomerID as it defaults to subscription customer
if *req.InvoiceBilling == types.InvoiceBillingInvoiceToParent {
Copy link
Member

Choose a reason for hiding this comment

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

should use lo.FromPtr never dereference direclty

@omkar273 omkar273 merged commit 58b2263 into flexprice:develop Dec 2, 2025
2 of 3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 2, 2025
@omkar273 omkar273 deleted the feat/customer-hierarchy branch February 9, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants