Repository: gagigante/gofer-app
Pull request #86: test: implement tests for customers controller
Author: login: gagigante
Created: 2025-05-27T23:07:25Z | Merged: 2025-05-27T23:20:10Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved input validation and error handling when creating or updating customers, ensuring fields are properly checked and formatted.
	- Enhanced error messages for invalid or missing customer data.
- **Tests**
	- Added comprehensive tests for all customer operations, including listing, retrieving, creating, updating, and deleting customers, as well as permission checks and error scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/api/controllers/customers-controller.test.ts b/src/api/controllers/customers-controller.test.ts
new file mode 100644
index 0000000..0b71d56
--- /dev/null
+++ b/src/api/controllers/customers-controller.test.ts
@@ -0,0 +1,479 @@
+import { describe, test, expect, beforeEach } from 'vitest'
+import { eq } from 'drizzle-orm'
+
+import { db } from '@/api/db/client'
+import { Customer, customers, users } from '@/api/db/schema'
+
+import { CustomersController } from './customers-controller'
+
+import { WithoutPermissionError } from '../errors/WithoutPermissionError'
+import { NotFoundError } from '../errors/NotFoundError'
+import { InvalidParamsError } from '../errors/InvalidParamsError'
+
+describe('customers-controller', () => {
+  const customersController = new CustomersController()
+
+  beforeEach(async () => {
+    await db.insert(users).values({
+      id: 'test-user-id',
+      name: 'test-user',
+      password: 'test-user-password',
+      role: 'super-admin',
+    })
+  })
+
+  describe('listCustomers', () => {
+    test('should throw WithoutPermissionError if loggedUserId does not correspond to an user', async () => {
+      const response = await customersController.listCustomers({
+        loggedUserId: 'non-existing-user-id',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(WithoutPermissionError)
+    })
+
+    test('should be able to list customers', async () => {
+      const CUSTOMERS: Customer[] = [
+        {
+          id: 'customer-3',
+          name: 'Bob Johnson',
+          email: 'bob@example.com',
+          phone: '5555555555',
+          cpf: null,
+          rg: null,
+          cnpj: null,
+          ie: null,
+          zipcode: null,
+          city: null,
+          complement: null,
+          street: null,
+          neighborhood: null,
+        },
+        {
+          id: 'customer-2',
+          name: 'Jane Smith',
+          email: 'jane@example.com',
+          phone: '0987654321',
+          cpf: null,
+          rg: null,
+          cnpj: null,
+          ie: null,
+          zipcode: null,
+          city: null,
+          complement: null,
+          street: null,
+          neighborhood: null,
+        },
+        {
+          id: 'customer-1',
+          name: 'John Doe',
+          email: 'john@example.com',
+          phone: '1234567890',
+          cpf: null,
+          rg: null,
+          cnpj: null,
+          ie: null,
+          zipcode: null,
+          city: null,
+          street: null,
+          neighborhood: null,
+          complement: null,
+        },
+      ]
+
+      await db.insert(customers).values(CUSTOMERS)
+
+      let response = await customersController.listCustomers({
+        loggedUserId: 'test-user-id',
+      })
+
+      expect(response.data?.customers).length(3)
+      expect(response.data?.customers).toStrictEqual(CUSTOMERS)
+      expect(response.data?.page).toBe(1)
+      expect(response.data?.total).toBe(3)
+      expect(response.data?.itemsPerPage).toBe(15)
+      expect(response.err).toBeNull()
+
+      response = await customersController.listCustomers({
+        loggedUserId: 'test-user-id',
+        page: 1,
+        itemsPerPage: 2,
+      })
+
+      expect(response.data?.customers).length(2)
+      expect(response.data?.customers).toStrictEqual(CUSTOMERS.slice(0, 2))
+      expect(response.data?.page).toBe(1)
+      expect(response.data?.total).toBe(3)
+      expect(response.data?.itemsPerPage).toBe(2)
+      expect(response.err).toBeNull()
+    })
+
+    test('should be able to list customers with filter by name', async () => {
+      const CUSTOMERS: Customer[] = [
+        {
+          id: 'customer-1',
+          name: 'John Doe',
+          email: 'john@example.com',
+          phone: '1234567890',
+          cpf: null,
+          rg: null,
+          cnpj: null,
+          ie: null,
+          zipcode: null,
+          city: null,
+          street: null,
+          neighborhood: null,
+          complement: null,
+        },
+        {
+          id: 'customer-2',
+          name: 'Jane Smith',
+          email: 'jane@example.com',
+          phone: '0987654321',
+          cpf: null,
+          rg: null,
+          cnpj: null,
+          ie: null,
+          zipcode: null,
+          city: null,
+          street: null,
+          neighborhood: null,
+          complement: null,
+        },
+      ]
+
+      await db.insert(customers).values(CUSTOMERS)
+
+      const response = await customersController.listCustomers({
+        loggedUserId: 'test-user-id',
+        name: 'John',
+      })
+
+      expect(response.data?.customers).length(1)
+      expect(response.data?.customers).toStrictEqual([CUSTOMERS[0]])
+      expect(response.data?.page).toBe(1)
+      expect(response.data?.total).toBe(1)
+      expect(response.data?.itemsPerPage).toBe(15)
+      expect(response.err).toBeNull()
+    })
+  })
+
+  describe('getCustomer', () => {
+    test('should throw WithoutPermissionError if loggedUserId does not correspond to an user', async () => {
+      const response = await customersController.getCustomer({
+        loggedUserId: 'non-existing-user-id',
+        customerId: 'customer-id',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(WithoutPermissionError)
+    })
+
+    test('should throw NotFoundError if the provided customer id does not correspond to an existing customer', async () => {
+      const response = await customersController.getCustomer({
+        loggedUserId: 'test-user-id',
+        customerId: 'non-existing-customer-id',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(NotFoundError)
+    })
+
+    test('should be able to get a customer by its ID', async () => {
+      const customer: Customer = {
+        id: 'customer-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        complement: null,
+        street: null,
+        neighborhood: null,
+      }
+
+      await db.insert(customers).values(customer)
+
+      const response = await customersController.getCustomer({
+        loggedUserId: 'test-user-id',
+        customerId: 'customer-id',
+      })
+
+      expect(response.data).toStrictEqual(customer)
+      expect(response.err).toBeNull()
+    })
+  })
+
+  describe('createCustomer', () => {
+    test('should throw WithoutPermissionError if loggedUserId does not correspond to an user', async () => {
+      const response = await customersController.createCustomer({
+        loggedUserId: 'non-existing-user-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(WithoutPermissionError)
+    })
+
+    test('should create a customer with success', async () => {
+      const response = await customersController.createCustomer({
+        loggedUserId: 'test-user-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+      })
+
+      expect(response.data?.name).toBe('John Doe')
+      expect(response.data?.email).toBe('john@example.com')
+      expect(response.data?.phone).toBe('1234567890')
+      expect(response.err).toBeNull()
+    })
+
+    test('should reject empty customer names', async () => {
+      const response = await customersController.createCustomer({
+        loggedUserId: 'test-user-id',
+        name: '',
+        email: 'john@example.com',
+        phone: '1234567890',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(InvalidParamsError)
+    })
+
+    test('should reject invalid email format', async () => {
+      const response = await customersController.createCustomer({
+        loggedUserId: 'test-user-id',
+        name: 'John Doe',
+        email: 'invalid-email',
+        phone: '1234567890',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(InvalidParamsError)
+    })
+  })
+
+  describe('deleteCustomer', () => {
+    test('should throw WithoutPermissionError if loggedUserId does not correspond to an user', async () => {
+      const response = await customersController.deleteCustomer({
+        loggedUserId: 'non-existing-user-id',
+        customerId: 'customer-id',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(WithoutPermissionError)
+    })
+
+    test('should throw NotFoundError if the provided customer id does not correspond to an existing customer', async () => {
+      const response = await customersController.deleteCustomer({
+        loggedUserId: 'test-user-id',
+        customerId: 'customer-id',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(NotFoundError)
+    })
+
+    test('should be able to delete a customer', async () => {
+      await db.insert(customers).values({
+        id: 'customer-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+      })
+
+      const response = await customersController.deleteCustomer({
+        loggedUserId: 'test-user-id',
+        customerId: 'customer-id',
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeNull()
+
+      const customer = await db.select().from(customers).where(eq(customers.id, 'customer-id')).get()
+
+      expect(customer).toBeUndefined()
+    })
+  })
+
+  describe('updateCustomer', () => {
+    test('should throw WithoutPermissionError if loggedUserId does not correspond to an user', async () => {
+      const response = await customersController.updateCustomer({
+        loggedUserId: 'non-existing-user-id',
+        id: 'customer-id',
+        name: 'Updated Name',
+        email: 'updated@example.com',
+        phone: '9876543210',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(WithoutPermissionError)
+    })
+
+    test('should throw NotFoundError if the customer does not exist', async () => {
+      const response = await customersController.updateCustomer({
+        loggedUserId: 'test-user-id',
+        id: 'non-existing-customer-id',
+        name: 'Updated Name',
+        email: 'updated@example.com',
+        phone: '9876543210',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(NotFoundError)
+    })
+
+    test('should be able to update a customer', async () => {
+      await db.insert(customers).values({
+        id: 'customer-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      const response = await customersController.updateCustomer({
+        loggedUserId: 'test-user-id',
+        id: 'customer-id',
+        name: 'Updated Name',
+        email: 'updated@example.com',
+        phone: '9876543210',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      expect(response.data).toStrictEqual({
+        id: 'customer-id',
+        name: 'Updated Name',
+        email: 'updated@example.com',
+        phone: '9876543210',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+      expect(response.err).toBeNull()
+    })
+
+    test('should reject empty customer names during update', async () => {
+      await db.insert(customers).values({
+        id: 'customer-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      const response = await customersController.updateCustomer({
+        loggedUserId: 'test-user-id',
+        id: 'customer-id',
+        name: '',
+        email: 'updated@example.com',
+        phone: '9876543210',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(InvalidParamsError)
+    })
+
+    test('should reject invalid email format during update', async () => {
+      await db.insert(customers).values({
+        id: 'customer-id',
+        name: 'John Doe',
+        email: 'john@example.com',
+        phone: '1234567890',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      const response = await customersController.updateCustomer({
+        loggedUserId: 'test-user-id',
+        id: 'customer-id',
+        name: 'Updated Name',
+        email: 'invalid-email',
+        phone: '9876543210',
+        cpf: null,
+        rg: null,
+        cnpj: null,
+        ie: null,
+        zipcode: null,
+        city: null,
+        street: null,
+        neighborhood: null,
+        complement: null,
+      })
+
+      expect(response.data).toBeNull()
+      expect(response.err).toBeInstanceOf(InvalidParamsError)
+    })
+  })
+})
diff --git a/src/api/controllers/customers-controller.ts b/src/api/controllers/customers-controller.ts
index 8784e85..ed9ae7f 100644
--- a/src/api/controllers/customers-controller.ts
+++ b/src/api/controllers/customers-controller.ts
@@ -1,8 +1,10 @@
 import { randomUUID } from 'node:crypto'
+import * as z from 'zod'
 
 import { UsersRepository } from '../repositories/users-repository'
 import { CustomersRepository } from '@/api/repositories/customers-repository'
 
+import { InvalidParamsError } from '@/api/errors/InvalidParamsError'
 import { WithoutPermissionError } from '@/api/errors/WithoutPermissionError'
 import { NotFoundError } from '@/api/errors/NotFoundError'
 
@@ -112,11 +114,31 @@ export class CustomersController {
       return { data: null, err }
     }
 
-    const createdCustomer = await this.customersRepository.createCustomer({
+    const customerWithTrimmedFields = Object.entries(newCustomer).reduce(
+      (acc, [key, value]) => {
+        acc[key as keyof Omit<NewCustomer, 'id'>] = value?.trim() || null
+
+        return acc
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
+
+    const response = this.validateCustomerData(customerWithTrimmedFields)
+    if (!response) {
+      const err = new InvalidParamsError()
+
+      return { data: null, err }
+    }
+
+    const { data: createdCustomer, err } = await this.customersRepository.createCustomer({
       id: randomUUID(),
-      ...newCustomer,
+      ...customerWithTrimmedFields,
     })
 
+    if (err) {
+      return { data: null, err }
+    }
+
     return { data: createdCustomer, err: null }
   }
 
@@ -142,10 +164,7 @@ export class CustomersController {
     return { data: null, err: null }
   }
 
-  public async updateCustomer({
-    loggedUserId,
-    ...updatedCustomer
-  }: UpdateCustomerRequest): Promise<UpdateCustomerResponse> {
+  public async updateCustomer({ loggedUserId, ...customer }: UpdateCustomerRequest): Promise<UpdateCustomerResponse> {
     const loggedUser = await this.usersRepository.getUserById(loggedUserId)
 
     if (!loggedUser) {
@@ -154,8 +173,55 @@ export class CustomersController {
       return { data: null, err }
     }
 
-    const response = await this.customersRepository.updateCustomer(updatedCustomer)
+    const customerToUpdate = await this.customersRepository.getCustomerById(customer.id)
+
+    if (!customerToUpdate) {
+      const err = new NotFoundError()
+
+      return { data: null, err }
+    }
+
+    const customerWithTrimmedFields = Object.entries(customer).reduce(
+      (acc, [key, value]) => {
+        acc[key as keyof Omit<NewCustomer, 'id'>] = value?.trim() || null
+
+        return acc
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
+
+    const response = this.validateCustomerData(customerWithTrimmedFields)
+    if (!response) {
+      const err = new InvalidParamsError()
+
+      return { data: null, err }
+    }
+
+    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customer)
+
+    if (err) {
+      return { data: null, err }
+    }
+
+    return { data: updatedCustomer, err: null }
+  }
+
+  private validateCustomerData(data: Omit<NewCustomer, 'id'>): boolean {
+    const schema = z.object({
+      name: z.string().min(1),
+      rg: z.union([z.string().nullable(), z.undefined()]),
+      cpf: z.union([z.string().nullable(), z.undefined()]),
+      cnpj: z.union([z.string().nullable(), z.undefined()]),
+      ie: z.union([z.string().nullable(), z.undefined()]),
+      email: z.union([z.string().email().nullable(), z.undefined()]),
+      phone: z.union([z.string().nullable(), z.undefined()]),
+      zipcode: z.union([z.string().nullable(), z.undefined()]),
+      city: z.union([z.string().nullable(), z.undefined()]),
+      street: z.union([z.string().nullable(), z.undefined()]),
+      neighborhood: z.union([z.string().nullable(), z.undefined()]),
+      complement: z.union([z.string().nullable(), z.undefined()]),
+    })
 
-    return { data: response, err: null }
+    return schema.safeParse(data).success
   }
 }
diff --git a/src/api/repositories/customers-repository.ts b/src/api/repositories/customers-repository.ts
index 2c4e558..73c12a0 100644
--- a/src/api/repositories/customers-repository.ts
+++ b/src/api/repositories/customers-repository.ts
@@ -1,6 +1,11 @@
 import { asc, eq, like, count } from 'drizzle-orm'
+import { type LibsqlError } from '@libsql/client'
 
 import { db } from '@/api/db/client'
+
+import { RepositoryError } from '@/api/errors/RepositoryError'
+
+import { type Response } from '@/api/types/response'
 import { type NewCustomer, type Customer, customers } from '../db/schema'
 
 export class CustomersRepository {
@@ -31,19 +36,27 @@ export class CustomersRepository {
     return response.count
   }
 
-  public async createCustomer(data: NewCustomer): Promise<Customer> {
-    const [response] = await db.insert(customers).values(data).returning()
+  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.insert(customers).values(data).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
 
   public async deleteCustomer(customerId: string): Promise<void> {
     await db.delete(customers).where(eq(customers.id, customerId))
   }
 
-  public async updateCustomer({ id, ...rest }: Customer): Promise<Customer> {
-    const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()
+  public async updateCustomer({ id, ...rest }: Customer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/api/controllers/customers-controller.ts (2)</summary><blockquote>

`211-228`: **Consider optimizing the validation schema.**

The schema is recreated on every validation call. Consider defining it as a static property for better performance.



Define the schema as a private static property:

```typescript
private static readonly customerSchema = z.object({
  name: z.string().min(1),
  rg: z.union([z.string().nullable(), z.undefined()]),
  cpf: z.union([z.string().nullable(), z.undefined()]),
  cnpj: z.union([z.string().nullable(), z.undefined()]),
  ie: z.union([z.string().nullable(), z.undefined()]),
  email: z.union([z.string().email().nullable(), z.undefined()]),
  phone: z.union([z.string().nullable(), z.undefined()]),
  zipcode: z.union([z.string().nullable(), z.undefined()]),
  city: z.union([z.string().nullable(), z.undefined()]),
  street: z.union([z.string().nullable(), z.undefined()]),
  neighborhood: z.union([z.string().nullable(), z.undefined()]),
  complement: z.union([z.string().nullable(), z.undefined()]),
})

private validateCustomerData(data: Omit<NewCustomer, 'id'>): boolean {
  return CustomersController.customerSchema.safeParse(data).success
}
```

Also consider adding phone number format validation if applicable to your use case.

---

`139-141`: **Add test coverage for repository error handling.**

The error handling paths (lines 140 and 205) are not covered by tests. These paths handle repository errors during create and update operations.




Would you like me to generate test cases that simulate repository errors to improve coverage? This would involve mocking the repository to throw errors and verifying that the controller properly propagates them.


Also applies to: 204-206

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 140-140: src/api/controllers/customers-controller.ts#L140
Added line #L140 was not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 31751299c5b15aad34a775958e699fade8ba17d2 and 10640fbb4ef301b309cd9ab6ed8e600a9f18ed93.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/api/controllers/customers-controller.test.ts` (1 hunks)
* `src/api/controllers/customers-controller.ts` (4 hunks)
* `src/api/repositories/customers-repository.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/api/repositories/customers-repository.ts (4)</summary><blockquote>

<details>
<summary>src/api/db/schema.ts (3)</summary>

* `NewCustomer` (107-107)
* `Customer` (106-106)
* `customers` (91-105)

</details>
<details>
<summary>src/api/types/response.ts (1)</summary>

* `Response` (1-1)

</details>
<details>
<summary>src/api/db/client.ts (1)</summary>

* `db` (12-12)

</details>
<details>
<summary>src/api/errors/RepositoryError.ts (1)</summary>

* `RepositoryError` (3-13)

</details>

</blockquote></details>
<details>
<summary>src/api/controllers/customers-controller.ts (3)</summary><blockquote>

<details>
<summary>src/api/db/schema.ts (1)</summary>

* `NewCustomer` (107-107)

</details>
<details>
<summary>src/api/errors/InvalidParamsError.ts (1)</summary>

* `InvalidParamsError` (1-5)

</details>
<details>
<summary>src/api/errors/NotFoundError.ts (1)</summary>

* `NotFoundError` (1-5)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>src/api/repositories/customers-repository.ts</summary>

[warning] 45-46: src/api/repositories/customers-repository.ts#L45-L46
Added lines #L45 - L46 were not covered by tests

---

[warning] 59-60: src/api/repositories/customers-repository.ts#L59-L60
Added lines #L59 - L60 were not covered by tests

</details>
<details>
<summary>src/api/controllers/customers-controller.ts</summary>

[warning] 140-140: src/api/controllers/customers-controller.ts#L140
Added line #L140 was not covered by tests

---

[warning] 205-205: src/api/controllers/customers-controller.ts#L205
Added line #L205 was not covered by tests

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/api/controllers/customers-controller.ts</summary>

[error] 120-120: Avoid the use of spread (`...`) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in `.reduce`) because it causes a time complexity of `O(n^2)`.
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

---

[error] 188-188: Avoid the use of spread (`...`) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in `.reduce`) because it causes a time complexity of `O(n^2)`.
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/api/controllers/customers-controller.test.ts (1)</summary>

`1-479`: **Excellent test coverage!**

The test suite is comprehensive and well-structured:
- Covers all controller methods thoroughly
- Tests both happy paths and error scenarios
- Validates the new input validation logic
- Properly tests permission checks and error handling

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-27T23:13:44Z
user: login: gagigante
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-27T23:15:13Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-27T23:15:40Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/api/controllers/customers-controller.ts (1)</summary><blockquote>

`200-200`: _⚠️ Potential issue_

**Critical: Pass the normalized customer data to the repository.**

You're validating the trimmed fields but passing the original untrimmed `customer` object to the repository. This defeats the purpose of normalization.


Apply this diff:

```diff
-    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customer)
+    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customerWithTrimmedFields as Customer)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 10640fbb4ef301b309cd9ab6ed8e600a9f18ed93 and e3d63a6adb025278bdecf8d35ca12e27d6a6bd4b.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/api/controllers/customers-controller.ts` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/api/controllers/customers-controller.ts (3)</summary><blockquote>

<details>
<summary>src/api/db/schema.ts (1)</summary>

* `NewCustomer` (107-107)

</details>
<details>
<summary>src/api/errors/InvalidParamsError.ts (1)</summary>

* `InvalidParamsError` (1-5)

</details>
<details>
<summary>src/api/errors/NotFoundError.ts (1)</summary>

* `NotFoundError` (1-5)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>src/api/controllers/customers-controller.ts</summary>

[warning] 139-139: src/api/controllers/customers-controller.ts#L139
Added line #L139 was not covered by tests

---

[warning] 203-203: src/api/controllers/customers-controller.ts#L203
Added line #L203 was not covered by tests

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/api/controllers/customers-controller.ts (7)</summary>

`2-2`: **LGTM: Zod import for validation.**

Good addition of the Zod library for input validation.

---

`7-7`: **LGTM: InvalidParamsError import.**

Appropriate error class import for validation failures.

---

`117-124`: **LGTM: Performance improvement implemented.**

Good fix! The direct mutation of the accumulator avoids the O(n²) complexity that was previously caused by the spread operator. This addresses the past review feedback effectively.

---

`126-131`: **LGTM: Proper validation with error handling.**

Good implementation of input validation with appropriate error handling using the custom InvalidParamsError.

---

`133-140`: **LGTM: Improved repository error handling.**

Good implementation of structured error handling from the repository response. 

**Note:** Static analysis indicates line 139 lacks test coverage for the error path.


Consider adding a test case that covers the repository error scenario to improve test coverage for line 139.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 139-139: src/api/controllers/customers-controller.ts#L139
Added line #L139 was not covered by tests

</details>

</details>

---

`202-204`: **LGTM: Consistent error handling pattern.**

Good implementation of error handling consistent with other methods.

**Note:** Static analysis indicates line 203 lacks test coverage for the error path.


Consider adding a test case that covers the repository error scenario in updateCustomer to improve test coverage for line 203.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 203-203: src/api/controllers/customers-controller.ts#L203
Added line #L203 was not covered by tests

</details>

</details>

---

`209-226`: **LGTM: Well-designed validation schema.**

Excellent implementation of input validation with:
- Required name field validation
- Proper email format validation  
- Appropriate handling of nullable/undefined optional fields
- Clean boolean return for validation result

The Zod schema correctly reflects the business rules for customer data.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-27T23:21:16Z

Review comments:
diff_hunk: @@ -31,19 +36,27 @@
     return response.count
   }
 
-  public async createCustomer(data: NewCustomer): Promise<Customer> {
-    const [response] = await db.insert(customers).values(data).returning()
+  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.insert(customers).values(data).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
path: src/api/repositories/customers-repository.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Improve error handling with type-safe error checking.**

The error casting to `LibsqlError` is unsafe. Not all errors thrown by database operations are necessarily `LibsqlError` instances. This could lead to runtime errors if a different error type is thrown.




Apply this diff to handle errors more safely:

```diff
-  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
-    try {
-      const [response] = await db.insert(customers).values(data).returning()
-
-      return { data: response, err: null }
-    } catch (error) {
-      return { data: null, err: new RepositoryError(error as LibsqlError) }
-    }
+  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.insert(customers).values(data).returning()
+
+      return { data: response, err: null }
+    } catch (error) {
+      if (error && typeof error === 'object' && 'code' in error && 'message' in error) {
+        return { data: null, err: new RepositoryError(error as LibsqlError) }
+      }
+      // For non-LibsqlError errors, wrap in a generic error
+      return { data: null, err: new Error(`Database operation failed: ${error}`) }
+    }
```

Additionally, the error path (lines 45-46) lacks test coverage. Consider adding tests that simulate database errors.

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the definition of RepositoryError
rg "class RepositoryError" -A5

# Find all references to LibsqlError across the codebase
rg "LibsqlError"

# Show the imports and setup for createCustomer in customers-repository.ts
sed -n '1,30p' src/api/repositories/customers-repository.ts
```

Length of output: 2202

---



</details>

**Ensure safe type checking before casting to LibsqlError**

The current catch block in createCustomer unconditionally casts any thrown error to LibsqlError, which risks runtime failures if a different error type surfaces. Please update it to discriminate actual LibsqlError instances and fallback to a generic error otherwise:

Suggested changes in src/api/repositories/customers-repository.ts:

```diff
@@ public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
-  } catch (error) {
-    return { data: null, err: new RepositoryError(error as LibsqlError) }
-  }
+  } catch (error: unknown) {
+    // Narrow to LibsqlError before wrapping
+    if (
+      typeof error === 'object' &&
+      error !== null &&
+      'code' in error &&
+      'message' in error
+    ) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
+    // Fallback for non-LibsqlError errors
+    return {
+      data: null,
+      err: new Error(`Database operation failed: ${String(error)}`),
+    }
+  }
```

• Extract the type-guard into a reusable function (e.g. `isLibsqlError(error): error is LibsqlError`)  
• Apply the same pattern in products-repository.ts  
• Add unit tests to cover both the LibsqlError path and the generic-error fallback



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
    try {
      const [response] = await db.insert(customers).values(data).returning()

      return { data: response, err: null }
    } catch (error: unknown) {
      // Narrow to LibsqlError before wrapping
      if (
        typeof error === 'object' &&
        error !== null &&
        'code' in error &&
        'message' in error
      ) {
        return { data: null, err: new RepositoryError(error as LibsqlError) }
      }
      // Fallback for non-LibsqlError errors
      return {
        data: null,
        err: new Error(`Database operation failed: ${String(error)}`),
      }
    }
  }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 45-46: src/api/repositories/customers-repository.ts#L45-L46
Added lines #L45 - L46 were not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/api/repositories/customers-repository.ts around lines 39 to 47, the catch
block unsafely casts all errors to LibsqlError, which can cause runtime issues
if the error is of a different type. Fix this by creating a reusable type-guard
function isLibsqlError to check if the error is an instance of LibsqlError
before casting. Use this type-guard in the catch block to conditionally create
the RepositoryError with either the LibsqlError or a generic error fallback.
Also, replicate this pattern in products-repository.ts and add unit tests
covering both LibsqlError and generic error scenarios to ensure proper error
handling and test coverage.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-27T23:13:43Z
author_association: NONE
diff_hunk: @@ -31,19 +36,27 @@
     return response.count
   }
 
-  public async createCustomer(data: NewCustomer): Promise<Customer> {
-    const [response] = await db.insert(customers).values(data).returning()
+  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.insert(customers).values(data).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
 
   public async deleteCustomer(customerId: string): Promise<void> {
     await db.delete(customers).where(eq(customers.id, customerId))
   }
 
-  public async updateCustomer({ id, ...rest }: Customer): Promise<Customer> {
-    const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()
+  public async updateCustomer({ id, ...rest }: Customer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
path: src/api/repositories/customers-repository.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Apply the same error handling improvement here.**

This method has the same unsafe type casting issue as `createCustomer`.



Apply this diff to handle errors safely:

```diff
-  public async updateCustomer({ id, ...rest }: Customer): Promise<Response<Customer>> {
-    try {
-      const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()
-
-      return { data: response, err: null }
-    } catch (error) {
-      return { data: null, err: new RepositoryError(error as LibsqlError) }
-    }
+  public async updateCustomer({ id, ...rest }: Customer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()
+
+      return { data: response, err: null }
+    } catch (error) {
+      if (error && typeof error === 'object' && 'code' in error && 'message' in error) {
+        return { data: null, err: new RepositoryError(error as LibsqlError) }
+      }
+      // For non-LibsqlError errors, wrap in a generic error
+      return { data: null, err: new Error(`Database operation failed: ${error}`) }
+    }
```

The error path (lines 59-60) also lacks test coverage.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  public async updateCustomer({ id, ...rest }: Customer): Promise<Response<Customer>> {
    try {
      const [response] = await db.update(customers).set(rest).where(eq(customers.id, id)).returning()

      return { data: response, err: null }
    } catch (error) {
      if (error && typeof error === 'object' && 'code' in error && 'message' in error) {
        return { data: null, err: new RepositoryError(error as LibsqlError) }
      }
      // For non-LibsqlError errors, wrap in a generic error
      return { data: null, err: new Error(`Database operation failed: ${error}`) }
    }
  }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 59-60: src/api/repositories/customers-repository.ts#L59-L60
Added lines #L59 - L60 were not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/api/repositories/customers-repository.ts lines 53 to 61, the
updateCustomer method unsafely casts the caught error to LibsqlError, which can
cause runtime issues. Modify the catch block to safely handle the error by
checking its type before casting or wrapping it appropriately without direct
casting. Additionally, add test coverage for the error path to ensure this
handling is verified.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T23:13:43Z
author_association: NONE
diff_hunk: @@ -154,8 +174,56 @@
       return { data: null, err }
     }
 
-    const response = await this.customersRepository.updateCustomer(updatedCustomer)
+    const customerToUpdate = await this.customersRepository.getCustomerById(customer.id)
+
+    if (!customerToUpdate) {
+      const err = new NotFoundError()
+
+      return { data: null, err }
+    }
+
+    const customerWithTrimmedFields = Object.entries(customer).reduce(
+      (acc, [key, value]) => {
+        return {
+          ...acc,
+          [key]: value?.trim() || null,
+        }
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
+
+    const response = this.validateCustomerData(customerWithTrimmedFields)
+    if (!response) {
+      const err = new InvalidParamsError()
+
+      return { data: null, err }
+    }
+
+    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customer)
path: src/api/controllers/customers-controller.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Critical: Pass the normalized customer data to the repository.**

You're validating the trimmed fields but passing the original untrimmed `customer` object to the repository. This defeats the purpose of normalization.



Apply this diff:

```diff
-    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customer)
+    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customerWithTrimmedFields as Customer)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const { data: updatedCustomer, err } = await this.customersRepository.updateCustomer(customerWithTrimmedFields as Customer)
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/api/controllers/customers-controller.ts at line 202, the code passes the
original untrimmed customer object to the updateCustomer method, ignoring the
normalized data. Modify the code to pass the trimmed and validated customer data
instead of the original object to ensure the repository receives the normalized
input.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T23:13:44Z
author_association: NONE
diff_hunk: @@ -112,11 +114,32 @@
       return { data: null, err }
     }
 
-    const createdCustomer = await this.customersRepository.createCustomer({
+    const customerWithTrimmedFields = Object.entries(newCustomer).reduce(
+      (acc, [key, value]) => {
+        return {
+          ...acc,
+          [key]: value?.trim() || null,
+        }
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
path: src/api/controllers/customers-controller.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Optimize the reduce operation to avoid O(n²) complexity.**

The spread operator in the accumulator causes performance degradation. For better performance, mutate the accumulator object directly.



Apply this diff to improve performance:

```diff
-    const customerWithTrimmedFields = Object.entries(newCustomer).reduce(
-      (acc, [key, value]) => {
-        return {
-          ...acc,
-          [key]: value?.trim() || null,
-        }
-      },
-      {} as Omit<NewCustomer, 'id'>,
-    )
+    const customerWithTrimmedFields = Object.entries(newCustomer).reduce(
+      (acc, [key, value]) => {
+        acc[key] = value?.trim() || null
+        return acc
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 120-120: Avoid the use of spread (`...`) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in `.reduce`) because it causes a time complexity of `O(n^2)`.
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/api/controllers/customers-controller.ts around lines 117 to 125, the
reduce operation uses the spread operator on the accumulator object, causing
O(n²) complexity. To fix this, avoid spreading the accumulator in each iteration
and instead directly mutate the accumulator by assigning the trimmed value to
the key. Return the mutated accumulator at the end of each iteration to improve
performance.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit e3d63a6
created_at: 2025-05-27T23:13:44Z
author_association: NONE
diff_hunk: @@ -154,8 +174,56 @@
       return { data: null, err }
     }
 
-    const response = await this.customersRepository.updateCustomer(updatedCustomer)
+    const customerToUpdate = await this.customersRepository.getCustomerById(customer.id)
+
+    if (!customerToUpdate) {
+      const err = new NotFoundError()
+
+      return { data: null, err }
+    }
+
+    const customerWithTrimmedFields = Object.entries(customer).reduce(
+      (acc, [key, value]) => {
+        return {
+          ...acc,
+          [key]: value?.trim() || null,
+        }
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
path: src/api/controllers/customers-controller.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Optimize the reduce operation to avoid O(n²) complexity.**

Same performance issue as in `createCustomer`.



Apply this diff:

```diff
-    const customerWithTrimmedFields = Object.entries(customer).reduce(
-      (acc, [key, value]) => {
-        return {
-          ...acc,
-          [key]: value?.trim() || null,
-        }
-      },
-      {} as Omit<NewCustomer, 'id'>,
-    )
+    const customerWithTrimmedFields = Object.entries(customer).reduce(
+      (acc, [key, value]) => {
+        acc[key] = value?.trim() || null
+        return acc
+      },
+      {} as Customer,
+    )
```

Note: The type should be `Customer` instead of `Omit<NewCustomer, 'id'>` since we're processing the full customer object including the id.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const customerWithTrimmedFields = Object.entries(customer).reduce(
      (acc, [key, value]) => {
        acc[key] = value?.trim() || null
        return acc
      },
      {} as Customer,
    )
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 188-188: Avoid the use of spread (`...`) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in `.reduce`) because it causes a time complexity of `O(n^2)`.
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/api/controllers/customers-controller.ts lines 185 to 193, the current
reduce operation creates a new object on each iteration causing O(n²)
complexity. To fix this, replace the reduce with a single loop that mutates an
accumulator object directly to achieve O(n) complexity. Also, change the
accumulator's type from Omit<NewCustomer, 'id'> to Customer since the full
customer object including id is processed.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T23:13:44Z
author_association: NONE
diff_hunk: @@ -31,19 +36,27 @@
     return response.count
   }
 
-  public async createCustomer(data: NewCustomer): Promise<Customer> {
-    const [response] = await db.insert(customers).values(data).returning()
+  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.insert(customers).values(data).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
path: src/api/repositories/customers-repository.ts
user: login: gagigante
body: erros on this layer is always an instance of LibsqlError
created_at: 2025-05-27T23:15:13Z
author_association: OWNER
diff_hunk: @@ -31,19 +36,27 @@
     return response.count
   }
 
-  public async createCustomer(data: NewCustomer): Promise<Customer> {
-    const [response] = await db.insert(customers).values(data).returning()
+  public async createCustomer(data: NewCustomer): Promise<Response<Customer>> {
+    try {
+      const [response] = await db.insert(customers).values(data).returning()
 
-    return response
+      return { data: response, err: null }
+    } catch (error) {
+      return { data: null, err: new RepositoryError(error as LibsqlError) }
+    }
   }
path: src/api/repositories/customers-repository.ts
user: login: coderabbitai[bot]
body: @gagigante Thank you for the clarification! I understand that at the repository layer, all errors are guaranteed to be LibsqlError instances, making the type casting safe in this context. This architectural knowledge is valuable for understanding the error handling patterns in your codebase.



      /|   /|  
     ( :v:  )
    o_)     (_o

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: gagigante
PR: gagigante/gofer-app#86
File: src/api/repositories/customers-repository.ts:39-47
Timestamp: 2025-05-27T23:15:38.893Z
Learning: In the repository layer of the gofer-app codebase, all errors thrown are always instances of LibsqlError, making type casting to LibsqlError safe in repository catch blocks.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@gagigante Thank you for the clarification! I understand that at the repository layer, all errors are guaranteed to be LibsqlError instances, making the type casting safe in this context. This architectural knowledge is valuable for understanding the error handling patterns in your codebase.



      /|   /|  
     ( :v:  )
    o_)     (_o

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: gagigante
PR: gagigante/gofer-app#86
File: src/api/repositories/customers-repository.ts:39-47
Timestamp: 2025-05-27T23:15:38.893Z
Learning: In the repository layer of the gofer-app codebase, all errors thrown are always instances of LibsqlError, making type casting to LibsqlError safe in repository catch blocks.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-27T23:15:39Z
author_association: NONE
diff_hunk: @@ -154,8 +173,55 @@
       return { data: null, err }
     }
 
-    const response = await this.customersRepository.updateCustomer(updatedCustomer)
+    const customerToUpdate = await this.customersRepository.getCustomerById(customer.id)
+
+    if (!customerToUpdate) {
+      const err = new NotFoundError()
+
+      return { data: null, err }
+    }
+
+    const customerWithTrimmedFields = Object.entries(customer).reduce(
+      (acc, [key, value]) => {
+        acc[key as keyof Omit<NewCustomer, 'id'>] = value?.trim() || null
+
+        return acc
+      },
+      {} as Omit<NewCustomer, 'id'>,
+    )
path: src/api/controllers/customers-controller.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix type mismatch in accumulator.**

The accumulator type should be `Customer` instead of `Omit<NewCustomer, 'id'>` since you're processing the full customer object including the `id` field.

Apply this diff to fix the type mismatch:

```diff
-      {} as Omit<NewCustomer, 'id'>,
+      {} as Customer,
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const customerWithTrimmedFields = Object.entries(customer).reduce(
      (acc, [key, value]) => {
        acc[key as keyof Omit<NewCustomer, 'id'>] = value?.trim() || null

        return acc
      },
      {} as Customer,
    )
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/api/controllers/customers-controller.ts around lines 184 to 191, the
accumulator in the reduce function is incorrectly typed as Omit<NewCustomer,
'id'>, but it should be typed as Customer to include the full customer object
with the id field. Update the accumulator type from Omit<NewCustomer, 'id'> to
Customer to fix the type mismatch.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-27T23:21:15Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-27T23:07:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces comprehensive unit tests for the `CustomersController`, adds input validation and normalization using Zod schemas for customer creation and update, and enhances error handling in both the controller and repository layers. The repository methods now return structured responses with explicit error information instead of throwing exceptions.

## Changes

| Files/Groups                                      | Change Summary                                                                                                                                                                                                                                                                                                                                                                 |
|-------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| src/api/controllers/customers-controller.test.ts | New test suite added for `CustomersController`, covering all public methods with tests for permissions, input validation, error handling, and correct data mutation and retrieval.                                                                                                                                                                                             |
| src/api/controllers/customers-controller.ts      | Enhanced `CustomersController` with input trimming, normalization, and Zod-based validation for create/update methods; added a private `validateCustomerData` method; updated error handling and internal logic for create and update operations.                                                                                                                             |
| src/api/repositories/customers-repository.ts     | Updated `createCustomer` and `updateCustomer` methods to wrap database operations in try-catch blocks, returning structured `{ data, err }` responses instead of throwing; updated method signatures to reflect new return types; unchanged `deleteCustomer` method.                                                                    |

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEejqANiS41EnFM25W2GXJFu5kAM3x8G2OxZKZCZXCnwLKwoDADlsZgFKLgAOADYDAFVEJMgiNCJ4PNcSI31jcCgyenwvHAJiMmUaeiZWdi5efmFRcSkZeSYlKlV1LR0ykyg4VFRMOsJScipmhTbXLioAd0hEeOY0Cnk5BSGVNU1tXTBDctMDRAoGAHo0bngn0NxwyOCPgII2BREGBPt8ohoPBDEBwDAAieEGADEiMgAEEAJINJbUOg7PYHeQ1RiwTCkRBmBDIcjbDyQLzwKx03zuWAkSAAAwAwv8gkDOfgwhEouyFFIgateCRWRhEJI2dgMOp3NJPEy+GhIpB9vAsP5AoCwBQSBYcdVuE14ALoZALEhxBgiIweYDEAAaSBGr7wEgSHWOtA7P2MvUAyjuhhG6h+91KKz2ojuzD0bDcWhRh1O/XBDSQdFuALSFlsgBq6hVyrsfrpVDYm18AGt0Bh6AHaWncCo0NlGLb2BoKWyPMgxfAvANfEaxJBEiTfcyk5BKOE+CTm7aMz4+CQ0AxYApBT8+GxcLBFDmAGLM22VjMh3lulBuIc8SjMJCygXE0T1h/cfI6qMBUTZs6QZGgKCrY4MDQNgL2ZT0IJ9KsAzvQF3SVZ9zQoN9EA/LAJA1eB20tLAF1XWh10dIkMAFMASAADztdhM1DIE4L8SN4xY+90KfFVkCwnC8K/Bgf3dXYGAYaREC8bALEYTiSOA+gCNtYjPyXXxvGZEhHFweRoLYZAFx1VSiMXbULEQdjIFjEguNQ4JeIrVVBPfEiRLEyByMo/hahojA6MYuxmMcoFlLxSTpNk+S7PEAUbJTYjb2dJzHxcgTX3cz9d2/B8fKrajaIYpjXG4l1xOwKLcJiyAkpxB8FzM9SsE08VNws7h9MgQzCxMjBmos7QrP7AwAFEdz3WkdWyChVXIKTcIJOrZqbFpUr4dsAx1Agixc2zqE7btEk3NlTPwesq1BIVKC1ezT1oayYFZDL0FwygnxJT62U9bAKHIegtq1ahcuQBjzTEQCZTWllqHQbheHwXh4BxcrFwoZdkAACnZAB1dRTzwAAFLLcJIsaMd8dl3XZGJ8FwS8FVoCnl2pjl0QGwjaCJg4YMQFmqYASnQI0WXCTYsE2aV91odQSOM0WaLcY8czgQd+PQKz8EgEcx1htwtoELs2TsVHd1JQt8Ekv63sXeiIZWNAvHAhTtxod16vdmG4rZJGLStUaAHkxQ1Cx3RPNl6UZRHfSUMHmzAeoqlFZRSD8vauQ2xB+QPYU6QVMQSMI/T0IwBgLGwWWMzcsmNIwTcpJcXBS+4PAda5qH3Ta7yk0oiKmAxnoDo7D17MQ1SYeYPAodGoxkTRCxwKh5Bdoj2zRBNZZ5fT8HfBWZlW4EW0GEXVw5ekUpIBiEgaXLXYy3QWglFoLh2TjiN4ESLGAHIwuBa7Dzf3dBoEBgsRQ6g5A8Z4rx3gAKiIgP4WYgQggFF8G6FAIQqihCKJgI4Myhx4NgI+8AT7HgesgIkmckHZ1QWCSgIpNgEy1HJcQTh1Z2EYMbbSW5Jp3RPGeOECIDAQDAEYKBLw3gfFoeghBf8UG50oFCGE8JYRIhRBiLETRcS7FYMtIk5sHQXwMGrDk3JqE5zQYeHBJpcKQE2F2LUihRzenoLtHU5dK5nQwK3NwiBMBywAF5Qxhs1YJHUwqu2CQuT2vssLLxzBzDkEY3YkDMaxNma93EsCuhtUCxpHoi0HBBNo1RahSzLIgP8UkYahDFCsXa7IMByQsOA2oukupHBIKdacJAqyhJWIwk86Aeo3x4BBAiNA+EPQ5P01JG0AAih0RQnjhgWYykAABaigdi5X2O4bWZAG4/RIAAR2wPAI0LYQJI3itBeS9J8n7jsFQHaD53EVyro6XSw01T7E0LmWo/SPJeGGo1LA7IObNR5jWfmlMKDgOQL9f6uJ4AlJRjQCwshVYvWScEhgBCaLbDjl8KquA/qFjXkaSpVpBzax8mybg9N2Ao3km1MG9tj6WFkO6JFioMzbgoJilAtRMCEmthgowiT2QxLSbyDJL0yHbMJaBIEbhdbehXi9EqIUy6+1qGvCJu0Yk8vsn9Pl/oOR0wZvgJmAt4XCq1O5B0CTvpYC9MwYyIEaLYUIgEilL0dQ+LyRYR67pZmat0nYpha8/FsB2ayfYEVeWevZpzNS0K+Z2tae3NSYTQXYvlKmVGfst6fg1IgOlvdCxGkZbKAghx0aYztk4EhXKC0zI7jQWVgJFkdhFIqwGXSdR9U2dsxAuyAwdQNbkh5IaHxGjORc5CPVip6QMjBNkC5Q74EYRmJpkQVCMmZEzIdAN+BdSLvc70c73SDL3N8hkOaiKmjVGMktvzqBBxDpEcOrJuwGLJCgQUtAqpslCLKHVbhA1tz8YqcQQTblhs7UpGG4NOVuG7gVDczIIm4tuTDGJ57/YylnmoheS9bkr21mvJQ5deaUZ3vbPeuID5EOPqfcQ4gjFQFRE/XEyMJlsgHR23NXaFmHSxltLggc3y4GANfTY3awyQG/kRb+ehBZcAEPgCI24MDgN1DY5AVDWI0IUfaiB7JxEwKkeZ2RWd5GWPBJ4dkwjIAZCLSsYTsoiDQTJaLShMqNoGZ7F2YzSm+TSKsUByBjwJGwKi/AxBpnHN0IwS5yAWM/w1nsrdI0vV6BeHCMwDkABvG0+AiCkFoFkSg6JaDAJATE2gEXIAAF9lna3ZOViwlXqu1YoPVxrGgIkdajUMnalA7kVYKCfADdBBZuY8+2XE3nCh+fJendkeG5lIJC3R2xJn7wWLSyFqzcWbNwN+HIq76XEAiixjRQMvnqCbfm9ONuk3/oahmyQ4kFtaCLZUaUO41nJE1vwHW3wGrkv3kNCQWt6hfBYs8MohE88NGLC0fQHR+wG36NXGSAcSTOK7fSTDaVnmydyqmYoZAEC15HZdAAJQR5DpHhxrFhbsZQITTiHmuO1u8zxjbmRYaIDmAAQvTPcA6qQ7rsVQbgRYLnDyOrE4j9O3WHBBCDPcR8rY/hzIHLAElFoPgjvIXlTYugiCnHevaM0Pp0CeM18qt0gYnr4G/JZFOlwimyE+bW+6LDG6wCChk5Lf0kAGHrvabVwxdig0HjkAAZT+iATkWCzbepX6UIEBnZKzxH9bZA55htbuYgg7duAdz7vttsQ9+4xjg1B2hzXx7hUB02ur23W/0uaCh+q/1CfunTnnot5uFeKxyImxWkAkGABFvQnXZ/z+yMAVn1KZSL+Xyv/Zo8vBWCnCeVA1IdgkrEOS9tb9jS5Yi/2sf9AjTamhgXQndBRoY8Xprg/NGN70bbxEi7xzQsZ8CHzsbMpcbkhuYACyT+z2G2oszWr8EBf2XYsgZckSYmSCkmh0XACmEWmmkAc+2SG+y+IogASYQchoEnwYFYE7YRZ4EdgEE3xEFcCkE4SL5b6Mo75L4bR6D74QIHbhZZzF7s6l5nZg7vAQ5Q6IT2bULw4l7I7YJwEIE+ZIGForYvw0FsboGICYEnxBa4HlZETDZUpuBtZcDsEkHr674CFUG6HEJ0EGFYHGGsRYymENaQAgIaAWHtbWEbTEGcEL6b7SC8HkECFCGGbc5M7BDiFyGyBSEXbg5s5yEw5yKyEc4o73aCKqLCLXC3CVDXIip4ALCNDLC4itBNwbBoDbC47LTHCDDKAjAXDjCFGTCrCyYAD6REiA3RRovoN8dA3Rpsc0VwNwnRAAjAAAypAAAsMxXgqg8xXSAAzDMVMQIBsQAJwMC0A7EqCpB0DJAkCpAzEzFoA7FeBTGnEHFrETEGCdEkBrG0CpBrFoCpBoC0ACAzEABMAArH8QAOzJACC0ZeDJC0BrEAn4pTF/EkAglvGfHgnzECCPFPEVBdHqC9GPQDFITDG0DdHJz6BAA= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=gagigante/gofer-app&utm_content=86):

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

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

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

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

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

### CodeRabbit Configuration File (`.coderabbit.yaml`)

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

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-27T23:08:43Z
body: ## [Codecov](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante) Report
Attention: Patch coverage is `91.54930%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 10.22%. Comparing base [(`3175129`)](https://app.codecov.io/gh/gagigante/gofer-app/commit/31751299c5b15aad34a775958e699fade8ba17d2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante) to head [(`e3d63a6`)](https://app.codecov.io/gh/gagigante/gofer-app/commit/e3d63a6adb025278bdecf8d35ca12e27d6a6bd4b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante) | Patch % | Lines |
|---|---|---|
| [src/api/repositories/customers-repository.ts](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?src=pr&el=tree&filepath=src%2Fapi%2Frepositories%2Fcustomers-repository.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante#diff-c3JjL2FwaS9yZXBvc2l0b3JpZXMvY3VzdG9tZXJzLXJlcG9zaXRvcnkudHM=) | 71.42% | [4 Missing :warning: ](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante) |
| [src/api/controllers/customers-controller.ts](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?src=pr&el=tree&filepath=src%2Fapi%2Fcontrollers%2Fcustomers-controller.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante#diff-c3JjL2FwaS9jb250cm9sbGVycy9jdXN0b21lcnMtY29udHJvbGxlci50cw==) | 96.49% | [2 Missing :warning: ](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##            main      #86      +/-   ##
=========================================
+ Coverage   8.47%   10.22%   +1.75%     
=========================================
  Files        173      173              
  Lines       8976     9033      +57     
  Branches     332      373      +41     
=========================================
+ Hits         761      924     +163     
+ Misses      8215     8109     -106     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/gagigante/gofer-app/pull/86?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Gabriel+Gigante).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE