feat(customer): add support for customer hierarchy#803
feat(customer): add support for customer hierarchy#803omkar273 merged 10 commits intoflexprice:developfrom
Conversation
…itID handling in mutations and queries
… ID in customer creation and update processes
…r hierarchical representation
…ce validation logic in customer creation and update requests
…re it is either the customer or their parent
…esponse and enhance validation for expand fields
|
Caution Review failedThe pull request is closed. WalkthroughAdds 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
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Suggested labels
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (1)
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. Comment |
There was a problem hiding this comment.
Important
Looks good to me! 👍
Reviewed everything up to ebf4efb in 2 minutes and 2 seconds. Click for details.
- Reviewed
2582lines of code in31files - Skipped
0files when reviewing. - Skipped posting
2draft 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 by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
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
customerFilterFnnow filters onf.ParentCustomerIDs, butcopyCustomerdoesn’t copyParentCustomerID, so customers stored in-memory always haveParentCustomerID == nil. This makes parent‑based filters always fail in tests and diverges from real repository behavior.Recommend updating
copyCustomerto 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. Fori=0, this produces an empty/null character; fori=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 forCustomerExpandConfig/ nested customer expands
CustomerExpandConfigcorrectly allowsparent_customerwith no nested expands. If you intend to support expressions likecustomer.parent_customerwhen expanding from subscriptions, invoices, or alert logs, you’ll also need to addExpandParentCustomerinto the relevantNestedExpandsforExpandCustomerin those configs; otherwise this remains limited to top-level customer endpoints.ent/schema/priceunit.go (1)
75-77: Edge removal fromPriceUnitis coherent with scalarPriceUnitIDapproachReturning
nilfromEdges()cleanly removes graph edges fromPriceUnit. This matches the move away fromWithPrices/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_idwiring and ordering helper look correctAdding
FieldParentCustomerID, including it inColumns, and exposingByParentCustomerIDis consistent with the rest of the generated customer API. As hierarchies grow, you may want to ensure there’s an appropriate DB index onparent_customer_idif 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 semanticsUsing
SetNillableParentCustomerID(c.ParentCustomerID)on both create and update correctly propagates the pointer field through to Ent. One thing to confirm: on updates, anilhere means “do not touch DB value”, not “clear to NULL” — clearing would require an explicitClearParentCustomerID()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 slicesThe added filter fields (
Filters,Sort, ID slices, andParentCustomerIDs) are wired consistently with existing patterns (tags,omitempty, etc.). If you expect untrusted input here, consider optionally reusingValidateCustomerID/validateIDforCustomerIDs,ExternalIDs, andParentCustomerIDsto 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 enforcedThe new validation cleanly restricts
InvoicingCustomerIDto 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 lookupsThe
parent_customer_idfield 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
ParentCustomerIDregularly (e.g., viaParentCustomerIDInfilters), 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
GetCustomercall 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 providedent/migrate/schema.go (1)
673-673: Consider: No foreign key constraint on parent_customer_id.The
parent_customer_idcolumn is added without a foreign key constraint referencing thecustomerstable. 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.
📒 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)
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: NewExpandParentCustomerfield looks consistentThe new
ExpandParentCustomerconstant aligns with existingExpandableFieldusage and naming; no issues from a types/validation perspective.ent/priceunit_query.go (1)
333-337:sqlAllsimplification is correct; note loss of eager-loaded PricesThe updated
sqlAllthat just builds_specand appends into a plainnodesslice 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 byPriceUnitIDon the price side rather than relying onWithPrices.ent/mutation.go (4)
19209-19256: Generated mutation methods follow standard Ent patterns.These
parent_customer_idmutation methods correctly implement the standard Ent pattern for nullable string fields:
SetParentCustomerID/ParentCustomerIDfor value accessOldParentCustomerIDfor retrieving previous value during updatesClear/Cleared/Resetfor nullable field lifecycle managementThe 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_idis 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
PriceUnitMutationis 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_idfield is properly defined in the schema (likelyent/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 predicatesThe new
ParentCustomerIDshandling (len(f.ParentCustomerIDs) > 0→customer.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 scalarprice_unit_idmigrationHaving
Edges()returnnilhere is consistent with removing thePriceUnitedge in favor of the scalarprice_unit_idfield. 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 EntAdding
ParentCustomerID *stringto the domainCustomerand mapping it inFromEntkeeps 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 schemaSwitching
GroupIDto*stringplus the updated scan/assign andString()handling correctly aligns this model with a nullablegroup_idcolumn. Callers will now seenull/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
Precisionfield 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
SetParentCustomerIDandSetNillableParentCustomerIDmethods 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
ParentCustomerIDis properly serialized to the create spec and assigned as a pointer to_node.ParentCustomerID, consistent with how other optional ID fields (likeMeterID,EntityID) are handled elsewhere.ent/customer.go (3)
55-57: LGTM - ParentCustomerID field correctly defined.The nullable
ParentCustomerIDfield is properly typed as*stringwith 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
Validis 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
ParentCustomerIDis not set.ent/price_create.go (1)
695-698: LGTM - Direct field handling replaces edge-based relationship.The
PriceUnitIDis 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:
- Validates expand fields early
- Returns early for empty results
- Batches parent customer fetches to avoid N+1 queries
- 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 andfalse.
342-361: LGTM - Proper handling of parent customer assignment updates.The implementation correctly:
- Trims whitespace for robust comparison
- Only runs expensive validations when the hierarchy actually changes
- Distinguishes between clearing the parent (empty string → nil) and setting a new parent
579-633: Well-designed validation for parent-child hierarchy constraints.The
validateParentCustomerAssignmentmethod comprehensively enforces business rules:
- Active subscriptions check (lines 580-594): Prevents hierarchy changes that could complicate billing
- Self-reference guard (lines 601-605): Prevents circular self-assignment
- Nested hierarchy prevention (lines 607-615): Ensures only single-level parent-child relationships
- Parent-with-children guard (lines 617-630): Prevents a customer that has children from becoming a child itself
The use of
Limit = 1for 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_idandparent_customer_external_idis documented clearly and validated in theValidate()method.
152-157: Validation logic is correct.The mutual exclusivity check properly prevents both
ParentCustomerIDandParentCustomerExternalIDfrom being provided simultaneously.
181-194: LGTM - UpdateCustomerRequest validation properly implemented.Follows the same validation pattern as
CreateCustomerRequestwith correct error handling.
175-175: Verify: ParentCustomerExternalID resolution happens in service layer.
ToCustomer()only mapsParentCustomerIDdirectly. Ensure the service layer resolvesParentCustomerExternalIDtoParentCustomerIDbefore persisting.ent/customer/where.go (2)
147-150: LGTM:ParentCustomerIDalias is consistent with existing helpersThe public
ParentCustomerIDconvenience alias cleanly mirrors patterns likeTenantIDandEnvironmentID, correctly delegating toFieldEQ(FieldParentCustomerID, v)and aligning with the comment about being identical toParentCustomerIDEQ.
1252-1325: LGTM: Full predicate set forparent_customer_idmatches other string fieldsThe
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 asEnvironmentID*/ExternalID*, with consistent use ofFieldParentCustomerID.
…g customer logic and enhance validation for subscription creation
… and update related logic for improved clarity and consistency
There was a problem hiding this comment.
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.
📒 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
InvoiceBillingtype 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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
follow up : maintain these changes in an audit log system
internal/service/subscription.go
Outdated
| // 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 { |
There was a problem hiding this comment.
should use lo.FromPtr never dereference direclty
…improved null safety
Important
Adds customer hierarchy support and simplifies price unit model by removing price unit edges.
ParentCustomerIDtoCustomermodel inent/customer.goandent/schema/customer.go.CreateCustomerRequestandUpdateCustomerRequestindto/customer.goto include parent customer fields.customerServiceininternal/service/customer.go.PriceUnitEdgeand related logic froment/price.go,ent/priceunit.go, andent/schema/price.go.ent/mutation.goandent/price_query.go.CustomerFilterintypes/customer.goto support filtering byParentCustomerID.ExpandParentCustomertoCustomerExpandConfigintypes/expand.go.internal/testutil/inmemory_customer_store.go.This description was created by
for ebf4efb. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Changes
✏️ Tip: You can customize this high-level summary in your review settings.