Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions Modules/Clients/Enums/CommunicationType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

enum CommunicationType: string implements LabeledEnum
{
case EMAIL = 'email';
case PHONE = 'phone';
case FAX = 'fax';
case MOBILE = 'mobile';
case WHATSAPP = 'whatsapp';
case EMAIL = 'email';
case PHONE = 'phone';
case FAX = 'fax';
case MOBILE = 'mobile';
case WHATSAPP = 'whatsapp';
case INVOICE_CC = 'invoice_cc';

public static function values(): array
{
Expand All @@ -20,22 +21,24 @@ public static function values(): array
public function label(): string
{
return match ($this) {
self::EMAIL => 'Email',
self::PHONE => 'Phone',
self::FAX => 'Fax',
self::MOBILE => 'Mobile',
self::WHATSAPP => 'Whatsapp',
self::EMAIL => 'Email',
self::PHONE => 'Phone',
self::FAX => 'Fax',
self::MOBILE => 'Mobile',
self::WHATSAPP => 'Whatsapp',
self::INVOICE_CC => 'Invoice CC Email',
};
}

public function color(): string
{
return match ($this) {
self::EMAIL => 'info',
self::PHONE => 'primary',
self::FAX => 'gray',
self::MOBILE => 'success',
self::WHATSAPP => 'amber',
self::EMAIL => 'info',
self::PHONE => 'primary',
self::FAX => 'gray',
self::MOBILE => 'success',
self::WHATSAPP => 'amber',
self::INVOICE_CC => 'warning',
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Schemas;
use Filament\Schemas\Components\Fieldset;
Expand Down Expand Up @@ -194,6 +195,13 @@ public static function configure(Schema $schema): Schema
->label(trans('ip.last'))
->required(),
]),

TagsInput::make('email_cc')
->label(trans('ip.cc_email_addresses'))
->splitKeys([',', 'Tab', ' '])
->placeholder('cc@example.com')
->nestedRecursiveRules('email')
->helperText(trans('ip.cc_email_addresses_helper')),
]),
])
->columnSpan(1),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use Modules\Clients\Enums\RelationStatus;
use Modules\Clients\Enums\RelationType;
use Modules\Clients\Models\Relation;
use Modules\Clients\Services\CustomerService;
use Modules\Clients\Services\RelationService;
use Modules\Core\Enums\Permission;
use Modules\Core\Helpers\EnumHelper;

Expand Down Expand Up @@ -91,7 +91,7 @@ public static function configure(Table $table): Table
EditAction::make('edit')
->visible(fn () => auth()->user()?->can(Permission::EDIT_RELATIONS->value))
->action(function (Relation $record, array $data) {
app(CustomerService::class)->updateCustomer($record, $data);
app(RelationService::class)->updateRelation($record, $data);
})
->modalWidth('full'),
DeleteAction::make('delete')
Expand Down
16 changes: 16 additions & 0 deletions Modules/Clients/Models/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Modules\Clients\Database\Factories\RelationFactory;
use Modules\Clients\Enums\CommunicationType;
use Modules\Clients\Enums\RelationStatus;
use Modules\Clients\Enums\RelationType;
use Modules\Core\Models\Company;
Expand Down Expand Up @@ -45,6 +46,7 @@
* @property Contact $contact
* @property string|null $currency_code
* @property string|null $language
* @property array $email_cc
* @property Company $company
* @property Collection|Contact[] $contacts
* @property Collection|Expense[] $expenses
Expand All @@ -69,6 +71,8 @@ class Relation extends Model

protected $guarded = [];

protected $appends = ['email_cc'];

/*
|--------------------------------------------------------------------------
| Static Methods
Expand Down Expand Up @@ -113,6 +117,11 @@ public function communications(): MorphMany
return $this->morphMany(Communication::class, 'communicationable');
}

public function ccEmailCommunications(): MorphMany
{
return $this->communications()->where('communication_type', CommunicationType::INVOICE_CC->value);
}

public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
Expand Down Expand Up @@ -178,6 +187,13 @@ public function getCustomerEmailAttribute()
return $this->email;
}

public function getEmailCcAttribute(): array
{
return $this->ccEmailCommunications()
->pluck('communication_value')
->all();
}

/*public function getPrimaryContactAttribute(): string
{
return mb_trim($this->primary_ontact?->first_name . ' ' . $this->primary_contact?->last_name);
Expand Down
23 changes: 23 additions & 0 deletions Modules/Clients/Services/RelationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Modules\Clients\Enums\CommunicationType;
use Modules\Clients\Enums\RelationStatus;
use Modules\Clients\Enums\RelationType;
use Modules\Clients\Events\CustomerWasCreated;
Expand Down Expand Up @@ -51,6 +52,10 @@ public function createRelation(array $data): Relation
$this->syncCommunications($relation, $data['communications']);
}

if (isset($data['email_cc']) && is_array($data['email_cc'])) {
$this->syncCcEmails($relation, $data['email_cc']);
}

DB::commit();

event(new CustomerWasCreated());
Expand Down Expand Up @@ -92,6 +97,10 @@ public function updateRelation(Relation $relation, array $data): Relation
$this->syncCommunications($relation, $data['communications']);
}

if (isset($data['email_cc']) && is_array($data['email_cc'])) {
$this->syncCcEmails($relation, $data['email_cc']);
}

DB::commit();

event(new CustomerWasUpdated());
Expand Down Expand Up @@ -159,4 +168,18 @@ protected function syncCommunications(Relation $relation, array $communications)
$relation->communications()->delete();
$relation->communications()->createMany($communicationsToSync);
}

protected function syncCcEmails(Relation $relation, array $emails): void
{
$relation->ccEmailCommunications()->delete();

foreach ($emails as $email) {
$relation->communications()->create([
'company_id' => $relation->company_id,
'communication_type' => CommunicationType::INVOICE_CC->value,
'communication_value' => $email,
'is_primary' => false,
]);
}
}
}
94 changes: 94 additions & 0 deletions Modules/Clients/Tests/Feature/CustomersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Modules\Clients\Enums\CommunicationType;
use Modules\Clients\Enums\RelationStatus;
use Modules\Clients\Enums\RelationType;
use Modules\Clients\Filament\Company\Resources\Relations\Pages\CreateRelation;
Expand Down Expand Up @@ -426,6 +427,99 @@ public function it_only_lists_customers_for_the_current_tenant(): void
}
# endregion

# region cc emails
#[Test]
#[Group('crud')]
public function it_stores_and_retrieves_cc_emails_as_communications(): void
{
/* Arrange */
$cc = ['billing@acme.com', 'finance@acme.com'];

/* Act */
$customer = Relation::factory()->for($this->company)->create();
foreach ($cc as $email) {
$customer->communications()->create([
'company_id' => $this->company->id,
'communication_type' => CommunicationType::INVOICE_CC->value,
'communication_value' => $email,
'is_primary' => false,
]);
}
$loaded = Relation::find($customer->id);

/* Assert */
$this->assertEqualsCanonicalizing($cc, $loaded->email_cc);
foreach ($cc as $email) {
$this->assertDatabaseHas('communications', [
'communicationable_id' => $customer->id,
'communicationable_type' => Relation::class,
'communication_type' => CommunicationType::INVOICE_CC->value,
'communication_value' => $email,
]);
}
}

#[Test]
#[Group('crud')]
public function it_creates_a_customer_with_cc_emails_through_a_modal(): void
{
/* Arrange */
$payload = [
'company_name' => 'Beta LLC',
'relation_type' => RelationType::CUSTOMER,
'relation_status' => RelationStatus::ACTIVE,
'relation_number' => 'C123',
'registered_at' => Carbon::parse('2025-01-01')->toDateString(),
'email_cc' => ['billing@acme.com', 'finance@acme.com'],
];

/* Act */
$component = Livewire::actingAs($this->user)
->test(ListRelations::class)
->mountAction('create')
->fillForm($payload)
->callMountedAction()
->assertHasNoFormErrors();

/* Assert */
$component->assertSuccessful();

$customer = Relation::where('company_name', $payload['company_name'])->firstOrFail();

foreach ($payload['email_cc'] as $email) {
$this->assertDatabaseHas('communications', [
'communicationable_id' => $customer->id,
'communicationable_type' => Relation::class,
'communication_type' => CommunicationType::INVOICE_CC->value,
'communication_value' => $email,
]);
}
}

#[Test]
#[Group('crud')]
public function it_fails_through_a_modal_with_an_invalid_cc_email(): void
{
/* Arrange */
$payload = [
'company_name' => 'Beta LLC',
'relation_type' => RelationType::CUSTOMER,
'relation_status' => RelationStatus::ACTIVE,
'relation_number' => 'C123',
'registered_at' => Carbon::parse('2025-01-01')->toDateString(),
'email_cc' => ['not-an-email'],
];

/* Act & Assert */
Livewire::actingAs($this->user)
->test(ListRelations::class)
->mountAction('create')
->fillForm($payload)
->callMountedAction()
->assertHasFormErrors(['email_cc.0' => 'email']);
}
# endregion

# region spicy
# endregion
}
8 changes: 5 additions & 3 deletions resources/lang/en/ip.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
return [
#region CLIENTS MODULE
'alert_no_client_assigned' => 'No client assigned to this project.',
'cc_email_addresses' => 'CC Email Addresses',
'cc_email_addresses_helper' => 'These addresses will be automatically CC\'d on every email sent to this client.',
Comment on lines +6 to +7

@coderabbitai coderabbitai Bot Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not promise automatic CC delivery before it exists.

The PR explicitly defers the Mail::cc() pipeline, but this text says every client email will be CC’d. Users may rely on CC recipients receiving invoices when no delivery integration runs yet.

Proposed fix
-    'cc_email_addresses_helper'        => 'These addresses will be automatically CC\'d on every email sent to this client.',
+    'cc_email_addresses_helper'        => 'These addresses are stored with this client.',
📝 Committable suggestion

‼️ 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.

Suggested change
'cc_email_addresses' => 'CC Email Addresses',
'cc_email_addresses_helper' => 'These addresses will be automatically CC\'d on every email sent to this client.',
'cc_email_addresses' => 'CC Email Addresses',
'cc_email_addresses_helper' => 'These addresses are stored with this client.',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/lang/en/ip.php` around lines 6 - 7, Update the
cc_email_addresses_helper translation to avoid promising automatic CC delivery
while the Mail::cc() pipeline is deferred; describe the addresses only as
configured CC recipients or pending functionality, without claiming they will be
included on every email.

@KhawarMehfooz KhawarMehfooz Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is just a helper message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@nielsdrost7 what do you think about this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@KhawarMehfooz, understood — I'll leave it as is. Since the CC delivery pipeline is a follow-up item, just keep this in mind for when that lands, so the helper text stays accurate for users.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what do you think about this?

@KhawarMehfooz
Don't worry about it. Coderabbit is delusional sometimes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

'change_client' => 'Change Client',
'client' => 'Customer',
'client_access' => 'Customer Access',
Expand Down Expand Up @@ -757,7 +759,7 @@
'automatic_email_on_recur' => 'Automatic email on recur',
'balance' => 'balance',
'basic' => 'basic',
'avatar' => 'Avatar',
'avatar' => 'Avatar',
'basic_information' => 'basic_information',
'bcc' => 'bcc',
'billable_hours' => 'billable_hours',
Expand Down Expand Up @@ -805,7 +807,7 @@
'quote_standard_password' => 'Quote standard password',
'quote_totals' => 'Quote totals',
'requires_authentication' => 'Requires authentication',
'role' => 'Role',
'role' => 'Role',
'role_permissions_updated' => 'Role permissions updated',
'roles_sync_complete' => 'Roles sync complete',
'roles_updated' => 'Roles updated',
Expand All @@ -824,7 +826,7 @@
'smtp_server_address' => 'Smtp server address',
'smtp_username' => 'Smtp username',
'sunday' => 'Sunday',
'switch' => 'Switch',
'switch' => 'Switch',
'task_description' => 'Task description',
'task_finish_date' => 'Task finish date',
'tasks' => 'Tasks',
Expand Down
Loading