Skip to content

Conversation

@ricaelchiquetti
Copy link

@ricaelchiquetti ricaelchiquetti commented Oct 30, 2025

📋 Description

Adds Template management endpoints for WhatsApp Business templates.

🔗 Related Issue

Closes #(issue_number)

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • [X ] ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced
  • Tested with different connection types (if applicable)

Manual checks:

  • POST /template/create with valid TemplateDto creates template and stores metadata
  • POST /template/edit updates template fields via Meta API
  • DELETE /template/delete deletes template by name or hsm_id and performs best-effort local cleanup
  • GET /template/find retrieves templates for the instance
  • Validation via JSONSchema7 blocks invalid payloads
  • Error responses are standardized and include Meta error payload when applicable

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

  • Multi-tenant: All operations are scoped by instance through WAMonitoringService
  • Validation: Uses JSONSchema7 and DTO classes following Evolution patterns
  • Error handling: Service propagates Meta API errors with structured payloads; routes use createMetaErrorResponse
  • Database: On delete, performs best-effort cleanup of local template metadata via Prisma

Summary by Sourcery

Add update and delete operations for WhatsApp Business message templates

New Features:

  • Implement TemplateService.edit to update templates via Meta API
  • Implement TemplateService.delete to remove templates and perform local metadata cleanup
  • Expose POST /template/edit and DELETE /template/delete routes with guards and error handling

Enhancements:

  • Introduce TemplateEditDto and TemplateDeleteDto with JSONSchema7 validation
  • Wrap and propagate Meta API errors into standardized responses

Chores:

  • Apply formatting and semicolon tweaks in makeProxyAgentUndici

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 30, 2025

Reviewer's Guide

This PR adds support for updating and deleting WhatsApp Business templates by extending the TemplateService with dedicated edit/delete methods and HTTP helpers, wiring new API endpoints with validation and error handling in the router and controller, defining DTOs and JSONSchema validators, and includes minor formatting cleanup in the proxy utility.

Sequence diagram for template edit API flow

sequenceDiagram
  actor User
  participant "TemplateRouter"
  participant "TemplateController"
  participant "TemplateService"
  participant "Meta API"
  User->>"TemplateRouter": POST /template/edit
  "TemplateRouter"->>"TemplateController": editTemplate(instance, data)
  "TemplateController"->>"TemplateService": edit(instance, data)
  "TemplateService"->>"Meta API": requestEditTemplate(templateId, payload)
  "Meta API"-->>"TemplateService": response
  "TemplateService"-->>"TemplateController": response
  "TemplateController"-->>"TemplateRouter": response
  "TemplateRouter"-->>User: HTTP 200 OK / error
Loading

Sequence diagram for template delete API flow

sequenceDiagram
  actor User
  participant "TemplateRouter"
  participant "TemplateController"
  participant "TemplateService"
  participant "Meta API"
  participant "PrismaRepository"
  User->>"TemplateRouter": DELETE /template/delete
  "TemplateRouter"->>"TemplateController": deleteTemplate(instance, data)
  "TemplateController"->>"TemplateService": delete(instance, data)
  "TemplateService"->>"Meta API": requestDeleteTemplate({ name, hsm_id })
  "Meta API"-->>"TemplateService": response
  "TemplateService"->>"PrismaRepository": deleteMany({ where: ... })
  "PrismaRepository"-->>"TemplateService": cleanup result
  "TemplateService"-->>"TemplateController": response
  "TemplateController"-->>"TemplateRouter": response
  "TemplateRouter"-->>User: HTTP 200 OK / error
Loading

ER diagram for new and updated template DTOs

erDiagram
  TEMPLATE {
    string name
    string language
    string category
    any components
    string webhookUrl
  }
  TEMPLATE_EDIT {
    string templateId
    string category
    boolean allowCategoryChange
    number ttl
    any components
  }
  TEMPLATE_DELETE {
    string name
    string hsmId
  }
  TEMPLATE ||--|| TEMPLATE_EDIT : "edit"
  TEMPLATE ||--|| TEMPLATE_DELETE : "delete"
Loading

Class diagram for new and updated template DTOs and service methods

classDiagram
  class TemplateDto {
    +string name
    +string language
    +string category
    +any components
    +string webhookUrl
  }
  class TemplateEditDto {
    +string templateId
    +string category
    +boolean allowCategoryChange
    +number ttl
    +any components
  }
  class TemplateDeleteDto {
    +string name
    +string hsmId
  }
  class TemplateService {
    +edit(instance: InstanceDto, data: TemplateEditDto)
    +delete(instance: InstanceDto, data: TemplateDeleteDto)
    -requestEditTemplate(templateId: string, data: any)
    -requestDeleteTemplate(name: string, hsm_id: string)
  }
  TemplateService ..> TemplateEditDto
  TemplateService ..> TemplateDeleteDto
Loading

Class diagram for TemplateController with new methods

classDiagram
  class TemplateController {
    +editTemplate(instance: InstanceDto, data: TemplateEditDto)
    +deleteTemplate(instance: InstanceDto, data: TemplateDeleteDto)
    +findTemplate(instance: InstanceDto)
  }
  TemplateController ..> TemplateEditDto
  TemplateController ..> TemplateDeleteDto
Loading

File-Level Changes

Change Details Files
Implement template edit and delete operations in TemplateService
  • Added public 'edit' method to construct payload, invoke Meta API and propagate errors
  • Added public 'delete' method to call Meta API and perform best-effort local metadata cleanup
  • Introduced private requestEditTemplate and requestDeleteTemplate HTTP helpers with logging and error handling
src/api/services/template.service.ts
Expose edit and delete endpoints in router and controller
  • Added POST /template/edit and DELETE /template/delete routes with guards, validation and standardized error responses
  • Implemented editTemplate and deleteTemplate methods in TemplateController to delegate to service
src/api/routes/template.router.ts
src/api/controllers/template.controller.ts
Define DTOs and JSONSchema validators for template editing and deletion
  • Created TemplateEditDto and TemplateDeleteDto
  • Added templateEdit.schema.ts and templateDelete.schema.ts with JSONSchema7 rules
  • Imported new schemas in validate.schema.ts
src/api/dto/template.dto.ts
src/validate/templateEdit.schema.ts
src/validate/templateDelete.schema.ts
src/validate/validate.schema.ts
Refactor proxy agent utility formatting
  • Standardized semicolons and spacing in makeProxyAgentUndici
  • Consolidated protocol constant declarations
src/utils/makeProxyAgent.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • Consolidate the instance lookup and token/businessId assignment in TemplateService into a shared private helper to reduce duplication and improve maintainability.
  • Consider using PATCH instead of POST for the template edit endpoint to better align with RESTful update semantics.
  • Replace console.error in the router error handlers with the project's logger to ensure consistent logging practices and severity levels.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consolidate the instance lookup and token/businessId assignment in TemplateService into a shared private helper to reduce duplication and improve maintainability.
- Consider using PATCH instead of POST for the template edit endpoint to better align with RESTful update semantics.
- Replace console.error in the router error handlers with the project's logger to ensure consistent logging practices and severity levels.

## Individual Comments

### Comment 1
<location> `src/api/services/template.service.ts:107` </location>
<code_context>
+    if (typeof data.category === 'string') payload.category = data.category;
+    if (typeof data.allowCategoryChange === 'boolean') payload.allow_category_change = data.allowCategoryChange;
+    if (typeof data.ttl === 'number') payload.time_to_live = data.ttl;
+    if (data.components) payload.components = data.components;
+
+    const response = await this.requestEditTemplate(data.templateId, payload);
</code_context>

<issue_to_address>
**suggestion:** Check for empty array or object in components assignment.

The current logic excludes empty arrays or objects from assignment, which may be valid. Use a check like data.components !== undefined to allow empty values if appropriate.

```suggestion
    if (data.components !== undefined) payload.components = data.components;
```
</issue_to_address>

### Comment 2
<location> `src/api/services/template.service.ts:145-152` </location>
<code_context>
+
+    try {
+      // Best-effort local cleanup of stored template metadata
+      await this.prismaRepository.template.deleteMany({
+        where: {
+          OR: [
+            { name: data.name, instanceId: getInstance.id },
+            data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
+          ].filter(Boolean) as any,
+        },
+      });
</code_context>

<issue_to_address>
**suggestion:** Avoid using 'as any' in Prisma query filter.

Explicitly define the filter type or refactor the query to maintain type safety and prevent hidden type errors.

```suggestion
      const orFilter: Array<{ name?: string; templateId?: string; instanceId: string }> = [
        { name: data.name, instanceId: getInstance.id },
      ];
      if (data.hsmId) {
        orFilter.push({ templateId: data.hsmId, instanceId: getInstance.id });
      }
      await this.prismaRepository.template.deleteMany({
        where: {
          OR: orFilter,
        },
      });
```
</issue_to_address>

### Comment 3
<location> `src/api/services/template.service.ts:154-156` </location>
<code_context>
+        },
+      });
+    } catch (err) {
+      this.logger.warn(
+        `Failed to cleanup local template records after delete: ${(err as Error)?.message || String(err)}`,
+      );
</code_context>

<issue_to_address>
**suggestion:** Consider including error stack in log for better diagnostics.

Including the stack trace will provide more context for debugging cleanup failures.

```suggestion
      this.logger.warn(
        `Failed to cleanup local template records after delete: ${(err as Error)?.message || String(err)}\nStack: ${(err as Error)?.stack || 'No stack trace available.'}`,
      );
```
</issue_to_address>

### Comment 4
<location> `src/validate/templateEdit.schema.ts:31` </location>
<code_context>
+    category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] },
+    allowCategoryChange: { type: 'boolean' },
+    ttl: { type: 'number' },
+    components: { type: 'array' },
+  },
+  required: ['templateId'],
</code_context>

<issue_to_address>
**suggestion:** Specify item type for components array in schema.

Defining items: { type: 'object' } for the components array will ensure proper validation and prevent invalid data.

```suggestion
    components: { 
      type: 'array',
      items: { type: 'object' }
    },
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

if (typeof data.category === 'string') payload.category = data.category;
if (typeof data.allowCategoryChange === 'boolean') payload.allow_category_change = data.allowCategoryChange;
if (typeof data.ttl === 'number') payload.time_to_live = data.ttl;
if (data.components) payload.components = data.components;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Check for empty array or object in components assignment.

The current logic excludes empty arrays or objects from assignment, which may be valid. Use a check like data.components !== undefined to allow empty values if appropriate.

Suggested change
if (data.components) payload.components = data.components;
if (data.components !== undefined) payload.components = data.components;

Comment on lines +145 to +152
await this.prismaRepository.template.deleteMany({
where: {
OR: [
{ name: data.name, instanceId: getInstance.id },
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
].filter(Boolean) as any,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Avoid using 'as any' in Prisma query filter.

Explicitly define the filter type or refactor the query to maintain type safety and prevent hidden type errors.

Suggested change
await this.prismaRepository.template.deleteMany({
where: {
OR: [
{ name: data.name, instanceId: getInstance.id },
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
].filter(Boolean) as any,
},
});
const orFilter: Array<{ name?: string; templateId?: string; instanceId: string }> = [
{ name: data.name, instanceId: getInstance.id },
];
if (data.hsmId) {
orFilter.push({ templateId: data.hsmId, instanceId: getInstance.id });
}
await this.prismaRepository.template.deleteMany({
where: {
OR: orFilter,
},
});

category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] },
allowCategoryChange: { type: 'boolean' },
ttl: { type: 'number' },
components: { type: 'array' },
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Specify item type for components array in schema.

Defining items: { type: 'object' } for the components array will ensure proper validation and prevent invalid data.

Suggested change
components: { type: 'array' },
components: {
type: 'array',
items: { type: 'object' }
},

} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
@DavidsonGomes DavidsonGomes merged commit 1e3a235 into EvolutionAPI:develop Nov 7, 2025
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants