Skip to content

Sf import#672

Merged
ashwin31 merged 4 commits intomasterfrom
sf_import
Mar 18, 2026
Merged

Sf import#672
ashwin31 merged 4 commits intomasterfrom
sf_import

Conversation

@ashwin31
Copy link
Member

@ashwin31 ashwin31 commented Mar 18, 2026

Summary by CodeRabbit

  • New Features

    • Added line item support for estimates and recurring invoices with creation, editing, and deletion capabilities; subtotals and totals automatically calculated.
    • Enabled user creation with minimal information (email and role only).
  • Improvements

    • Made phone and address fields optional during user registration.
    • Profile name field is now read-only.
    • Enhanced error messaging for form submissions.
  • Chores

    • Updated frontend dependencies and improved component robustness.
Copilot AI review requested due to automatic review settings March 18, 2026 15:06
error:
'Failed to update profile: ' + (err instanceof Error ? err.message : 'Unknown error'),
data: { name, phone }
data: { phone }
Copy link

Choose a reason for hiding this comment

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

'object short notation' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

return fail(400, {
errors,
data: { name, phone }
data: { phone }
Copy link

Choose a reason for hiding this comment

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

'object short notation' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

error:
'Failed to update profile: ' + (err instanceof Error ? err.message : 'Unknown error'),
data: { name, phone }
data: { phone }
Copy link

Choose a reason for hiding this comment

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

'object short notation' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

return fail(400, {
errors,
data: { name, phone }
data: { phone }
Copy link

Choose a reason for hiding this comment

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

'object short notation' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

return value.flatMap((item) => collectErrorMessages(item));
}
if (typeof value === 'object') {
return Object.values(value).flatMap((item) => collectErrorMessages(item));
Copy link

Choose a reason for hiding this comment

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

'arrow function syntax (=>)' is only available in ES6 (use 'esversion: 6').

if (!value) return [];
if (typeof value === 'string') return [value];
if (Array.isArray(value)) {
return value.flatMap((item) => collectErrorMessages(item));
Copy link

Choose a reason for hiding this comment

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

'arrow function syntax (=>)' is only available in ES6 (use 'esversion: 6').

error:
'Failed to update profile: ' + (err instanceof Error ? err.message : 'Unknown error'),
data: { name, phone }
data: { phone }
Copy link

Choose a reason for hiding this comment

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

'object short notation' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

return fail(400, {
errors,
data: { name, phone }
data: { phone }
Copy link

Choose a reason for hiding this comment

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

'object short notation' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

return value.flatMap((item) => collectErrorMessages(item));
}
if (typeof value === 'object') {
return Object.values(value).flatMap((item) => collectErrorMessages(item));
Copy link

Choose a reason for hiding this comment

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

'arrow function syntax (=>)' is only available in ES6 (use 'esversion: 6').

if (!value) return [];
if (typeof value === 'string') return [value];
if (Array.isArray(value)) {
return value.flatMap((item) => collectErrorMessages(item));
Copy link

Choose a reason for hiding this comment

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

'arrow function syntax (=>)' is only available in ES6 (use 'esversion: 6').

@ashwin31 ashwin31 merged commit 24ccf66 into master Mar 18, 2026
6 of 7 checks passed
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a95d932-5b0e-4492-afbc-8258cc2f4b6d

📥 Commits

Reviewing files that changed from the base of the PR and between 6fa9fb9 and 3a57357.

⛔ Files ignored due to path filters (1)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • backend/common/serializer.py
  • backend/common/tests/test_users.py
  • backend/invoices/serializer.py
  • backend/invoices/tests/test_invoices_api.py
  • frontend/package.json
  • frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte
  • frontend/src/routes/(app)/invoices/estimates/+page.server.js
  • frontend/src/routes/(app)/invoices/estimates/+page.svelte
  • frontend/src/routes/(app)/invoices/recurring/+page.server.js
  • frontend/src/routes/(app)/invoices/recurring/+page.svelte
  • frontend/src/routes/(app)/profile/+page.server.js
  • frontend/src/routes/(app)/profile/+page.svelte
  • frontend/src/routes/(app)/users/+page.server.js
  • frontend/src/routes/(app)/users/+page.svelte
  • frontend/src/routes/(no-layout)/login/+page.server.js
  • frontend/src/routes/(no-layout)/login/+page.svelte
  • frontend/src/routes/(no-layout)/org/+page.svelte

📝 Walkthrough

Walkthrough

The changes introduce nested line item support for invoice creation (estimates and recurring invoices) across backend and frontend, relax phone field requirements in user registration, make user names read-only in profiles, simplify user creation payloads, and modernize form action handling in login and user management flows.

Changes

Cohort / File(s) Summary
User and Profile Serialization
backend/common/serializer.py, backend/common/tests/test_users.py
Made phone and address fields optional in UserCreateSwaggerSerializer; updated CreateProfileSerializer to not require phone field. Added test for minimal user creation with only email and role.
Invoice Line Item Backend
backend/invoices/serializer.py, backend/invoices/tests/test_invoices_api.py
Introduced EstimateLineItemCreateSerializer and RecurringInvoiceLineItemCreateSerializer for nested line item handling. Extended EstimateCreateSerializer and RecurringInvoiceCreateSerializer with line_items field, cross-field validation, and create/update methods to persist and recalculate totals. Added API tests verifying line item creation and total calculations.
Estimate Form and UI
frontend/src/routes/(app)/invoices/estimates/+page.server.js, frontend/src/routes/(app)/invoices/estimates/+page.svelte
Added lineItems parsing and transformation in form actions; renamed relationship fields to include _id suffix. Implemented comprehensive line item UI with create, edit, and delete functionality; added lineItemsSubtotal calculation and hidden form fields for all line item attributes and metadata.
Recurring Invoice Form and UI
frontend/src/routes/(app)/invoices/recurring/+page.server.js, frontend/src/routes/(app)/invoices/recurring/+page.svelte
Mirrored estimate changes with lineItems parsing and field renaming. Added line item section to recurring invoice drawer with editable grid, subtotal display, and serialization into hidden form fields.
User and Profile Management
frontend/src/routes/(app)/users/+page.server.js, frontend/src/routes/(app)/users/+page.svelte, frontend/src/routes/(app)/profile/+page.server.js, frontend/src/routes/(app)/profile/+page.svelte
Updated user creation to send only email and role; improved error message extraction from API responses. Made user name field read-only in profile with disabled input state. Added toast notification for successful member addition.
Login and Organization Pages
frontend/src/routes/(no-layout)/login/+page.server.js, frontend/src/routes/(no-layout)/login/+page.svelte, frontend/src/routes/(no-layout)/org/+page.svelte
Renamed magic link action handler from requestMagicLink to default; enhanced error handling and added CSS for error display. Added default data prop and improved robustness against missing organization data with optional chaining and keyed rendering.
Frontend Dependencies and UI Components
frontend/package.json, frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte
Bumped multiple devDependency and dependency versions. Updated SelectOption type to support optional id/name and new value/label properties; added normalizedOptions logic to harmonize option shapes and filter invalid entries.

Sequence Diagram(s)

sequenceDiagram actor User as Frontend User participant Form as Estimate/Invoice Form participant API as Backend API participant Serializer as Serializer (create/update) participant Model as Model/Database User->>Form: Fill estimate/invoice + line items Form->>Form: Serialize lineItems to JSON Form->>API: POST/PUT with line_items array API->>Serializer: validate(attrs) alt Validation fails Serializer-->>API: ValidationError API-->>Form: 400 response Form-->>User: Display validation errors else Validation passes Serializer->>Serializer: Extract nested line_items Serializer->>Model: Create/update main object (estimate/invoice) Serializer->>Model: Create/update LineItem instances Model-->>Serializer: Persist all records Serializer->>Serializer: _recalculate_totals() Note over Serializer: Compute subtotal, tax, total from line items Serializer->>Model: Save updated totals Model-->>Serializer: Success Serializer-->>API: Return serialized object API-->>Form: 201/200 response with nested line_items Form-->>User: Show success, display created estimate/invoice end 
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • revamp and upgrade to modern stack #562: Directly modifies UserCreateSwaggerSerializer and CreateProfileSerializer in backend/common/serializer.py, making phone and address fields optional—overlapping with user serialization changes in this PR.

Poem

🐰 Invoices bloom with line-item care,
Each estimate now holds items fair,
Phone fields soften, names held still,
Forms recalculate with totaling skill—
The CRM hops forward once more! 🌿

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sf_import
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR primarily expands invoice estimate/recurring support (line items + additional fields), improves a few SvelteKit pages’ robustness around $props() defaults and keyed rendering, and updates both backend serializers/tests and frontend dependencies to match the new payload shapes.

Changes:

  • Add line-item create/update support for Estimates and Recurring Invoices across frontend drawers, SvelteKit actions, DRF serializers, and backend tests.
  • Adjust login magic-link flow to use the default form action and surface client-side errors; make org/user pages more resilient (defaults, keyed {#each}).
  • Update user creation to allow minimal payload (email + role) and bump frontend dependencies.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/routes/(no-layout)/org/+page.svelte Default $props() data + keyed org list rendering
frontend/src/routes/(no-layout)/login/+page.svelte Magic-link UX tweaks + error display
frontend/src/routes/(no-layout)/login/+page.server.js Switch magic-link action to default
frontend/src/routes/(app)/users/+page.svelte Toast handling for add_user action result
frontend/src/routes/(app)/users/+page.server.js Improve API error message extraction; simplify user payload
frontend/src/routes/(app)/profile/+page.svelte Make name read-only / remove name editing
frontend/src/routes/(app)/profile/+page.server.js Remove name validation + payload fields
frontend/src/routes/(app)/invoices/recurring/+page.svelte Add line-item UI and hidden form fields for recurring invoices
frontend/src/routes/(app)/invoices/recurring/+page.server.js Send *_id relationship keys; parse lineItems JSON into line_items
frontend/src/routes/(app)/invoices/estimates/+page.svelte Add line-item UI and hidden form fields for estimates
frontend/src/routes/(app)/invoices/estimates/+page.server.js Send *_id relationship keys; parse lineItems JSON into line_items
frontend/src/lib/components/ui/editable-field/EditableMultiSelect.svelte Normalize option shapes to prevent keyed {#each} crashes
frontend/package.json Dependency bumps (SvelteKit/Svelte tooling, Sentry, etc.)
frontend/pnpm-lock.yaml Lockfile updates for bumped dependencies
backend/invoices/tests/test_invoices_api.py Add API tests for creating estimate/recurring with line items
backend/invoices/serializer.py Add nested line-item serializers + create/update handling
backend/common/tests/test_users.py Add test for minimal user create payload
backend/common/serializer.py Make phone/address fields optional for user creation docs/serializer
Files not reviewed (1)
  • frontend/pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (5)

frontend/src/routes/(app)/invoices/estimates/+page.server.js:279

  • JSON.parse(lineItemsJson) can throw if the hidden lineItems input is missing/corrupted/tampered with. Consider wrapping the parse in a try/catch and returning fail(400, { error: 'Invalid line items payload' }) with a clear message.
 // Parse line items JSON const lineItemsJson = form.get('lineItems')?.toString() || '[]'; const lineItems = JSON.parse(lineItemsJson); 

frontend/src/routes/(app)/invoices/recurring/+page.server.js:204

  • JSON.parse(lineItemsJson) can throw if the hidden lineItems input is missing/corrupted/tampered with, which will currently be caught by the outer catch and returned as a generic 400 error (or could become a 500 depending on upstream). Consider wrapping the parse in a try/catch and returning fail(400, { error: 'Invalid line items payload' }) with a clear message.
 // Parse line items JSON const lineItemsJson = form.get('lineItems')?.toString() || '[]'; const lineItems = JSON.parse(lineItemsJson); if (lineItems.length > 0) { 

frontend/src/routes/(app)/invoices/estimates/+page.server.js:290

  • discount_type is defaulted to 'fixed' here (and on line-item mapping), but the backend expects 'FIXED'/'PERCENTAGE' (uppercase) per DISCOUNT_TYPES. This will cause 400 responses when creating/updating estimates. Update the values sent to the API to match the backend enum.
 // Amounts discount_type: form.get('discountType')?.toString() || 'fixed', discount_value: form.get('discountValue')?.toString() || '0', tax_rate: form.get('taxRate')?.toString() || '0', currency: form.get('currency')?.toString() || 'USD', // Notes notes: form.get('notes')?.toString() || '', terms: form.get('terms')?.toString() || '', // Client address client_address_line: form.get('clientAddressLine')?.toString() || '', client_city: form.get('clientCity')?.toString() || '', client_state: form.get('clientState')?.toString() || '', client_postcode: form.get('clientPostcode')?.toString() || '', client_country: form.get('clientCountry')?.toString() || '', // Relationships account_id: form.get('accountId')?.toString() || null, contact_id: form.get('contactId')?.toString() || null, opportunity_id: form.get('opportunityId')?.toString() || null }; // Parse line items JSON const lineItemsJson = form.get('lineItems')?.toString() || '[]'; const lineItems = JSON.parse(lineItemsJson); if (lineItems.length > 0) { estimateData.line_items = lineItems.map((item, idx) => ({ name: item.name || item.description || '', description: item.description || '', quantity: item.quantity || 1, unit_price: item.unitPrice || '0', discount_type: item.discountType || 'fixed', discount_value: item.discountValue || '0', tax_rate: item.taxRate || '0', order: item.order || idx })); 

frontend/src/routes/(no-layout)/login/+page.server.js:198

  • This action returns { success: false, error: ... } as a normal 200 response when email is missing. With use:enhance, this comes back as result.type === 'success', so the page logic may treat it as a successful send. Consider returning fail(400, { error: 'Email is required' }) for validation errors (and optionally a consistent error shape), so the enhanced form gets result.type === 'failure' and the UI can show magicLinkError reliably.
 default: async ({ request }) => { const formData = await request.formData(); const email = formData.get('email'); if (!email) { return { success: false, error: 'Email is required' }; } 

frontend/src/routes/(app)/invoices/recurring/+page.server.js:213

  • discount_type is defaulted to 'fixed' here (and on line-item mapping), but the backend expects 'FIXED'/'PERCENTAGE' (uppercase) per DISCOUNT_TYPES. This will cause 400 responses when creating/updating recurring invoices. Update the values sent to the API to match the backend enum.
 currency: form.get('currency')?.toString() || 'USD', // Amounts discount_type: form.get('discountType')?.toString() || 'fixed', discount_value: form.get('discountValue')?.toString() || '0', tax_rate: form.get('taxRate')?.toString() || '0', // Notes notes: form.get('notes')?.toString() || '', terms: form.get('terms')?.toString() || '', // Relationships account_id: form.get('accountId')?.toString() || null, contact_id: form.get('contactId')?.toString() || null }; // Parse line items JSON const lineItemsJson = form.get('lineItems')?.toString() || '[]'; const lineItems = JSON.parse(lineItemsJson); if (lineItems.length > 0) { recurringData.line_items = lineItems.map((item, idx) => ({ description: item.description || '', quantity: item.quantity || 1, unit_price: item.unitPrice || '0', discount_type: item.discountType || 'fixed', discount_value: item.discountValue || '0', tax_rate: item.taxRate || '0', order: item.order || idx })); 

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 804 to +820
def create(self, validated_data):
line_items_data = validated_data.pop("line_items", [])
validated_data["org"] = self.org
return super().create(validated_data)

estimate = Estimate.objects.create(**validated_data)

for idx, item_data in enumerate(line_items_data):
EstimateLineItem.objects.create(
estimate=estimate,
org=self.org,
order=item_data.get("order", idx),
**{k: v for k, v in item_data.items() if k != "order"},
)

estimate.recalculate_totals()
estimate.save()
return estimate
Comment on lines +1036 to +1054
def update(self, instance, validated_data):
line_items_data = validated_data.pop("line_items", None)

for attr, value in validated_data.items():
setattr(instance, attr, value)

if line_items_data is not None:
instance.line_items.all().delete()
for idx, item_data in enumerate(line_items_data):
RecurringInvoiceLineItem.objects.create(
recurring_invoice=instance,
org=self.org or instance.org,
order=item_data.get("order", idx),
**{k: v for k, v in item_data.items() if k != "order"},
)

self._recalculate_totals(instance)
instance.save()
return instance
Comment on lines 21 to 32
function handleMagicLink() {
isSendingLink = true;
magicLinkError = '';
return async ({ result }) => {
isSendingLink = false;
if (result.type === 'success') {
if (result?.type === 'success') {
magicLinkSent = true;
} else if (result.type === 'failure') {
} else if (result?.type === 'failure') {
magicLinkError = result.data?.error || 'Something went wrong. Please try again.';
} else if (!result) {
magicLinkError = 'Something went wrong. Please try again.';
}
accountId: '',
contactId: '',
currency: 'USD',
discountType: 'fixed',

// Parse line items JSON
const lineItemsJson = form.get('lineItems')?.toString() || '[]';
const lineItems = JSON.parse(lineItemsJson);

// Parse line items JSON
const lineItemsJson = form.get('lineItems')?.toString() || '[]';
const lineItems = JSON.parse(lineItemsJson);
Comment on lines +822 to +840
def update(self, instance, validated_data):
line_items_data = validated_data.pop("line_items", None)

for attr, value in validated_data.items():
setattr(instance, attr, value)

if line_items_data is not None:
instance.line_items.all().delete()
for idx, item_data in enumerate(line_items_data):
EstimateLineItem.objects.create(
estimate=instance,
org=self.org or instance.org,
order=item_data.get("order", idx),
**{k: v for k, v in item_data.items() if k != "order"},
)

instance.recalculate_totals()
instance.save()
return instance
Comment on lines 1018 to +1034
def create(self, validated_data):
line_items_data = validated_data.pop("line_items", [])
validated_data["org"] = self.org
return super().create(validated_data)

recurring = RecurringInvoice.objects.create(**validated_data)

for idx, item_data in enumerate(line_items_data):
RecurringInvoiceLineItem.objects.create(
recurring_invoice=recurring,
org=self.org,
order=item_data.get("order", idx),
**{k: v for k, v in item_data.items() if k != "order"},
)

self._recalculate_totals(recurring)
recurring.save()
return recurring
contactId: '',
opportunityId: '',
currency: 'USD',
discountType: 'fixed',
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants