Skip to content

Class Name: SubscriptionController

Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/SubscriptionController.cls

API Name: SubscriptionController Type: Controller (LWC) Test Coverage: SubscriptionControllerTest (likely)

Business Purpose

The SubscriptionController class provides a thin LWC controller layer for subscription management operations. This supports: - Viewing active subscriptions for an account - Canceling multiple subscriptions at once - Clean separation between LWC controller and service/selector layers - Consistent exception handling for UI components

This follows the Controller → Service → Selector architectural pattern for clean code organization.

Class Overview

  • Author: Not specified
  • Created: Unknown
  • Test Class: SubscriptionControllerTest (likely)
  • Scope/Sharing: with sharing - Respects record-level security
  • Key Responsibilities:
  • Delegate to SubscriptionSelector for queries
  • Delegate to SubscriptionService for business logic
  • Wrap exceptions in AuraHandledException for LWC display
  • Return user-friendly success messages

Constants

final public static String SUBSCRIPTIONS_CANCELED_SUCCESSFUL_MESSAGE = 'Selected subscriptions have been successfully cancelled.';

Purpose: Centralized success message for cancellation (line 2).

Issues/Concerns: - ⚠️ Typo: "cancelled" (British spelling) - Should be "canceled" (American) for consistency - ✅ Constant Pattern: Good practice for UI messages

Public Methods

getActiveSubscriptions

@AuraEnabled(cacheable=true)
public static List<Subscription__c> getActiveSubscriptions(final String accountId)

Purpose: Retrieves all active subscriptions for a given account.

Parameters: - accountId (String) - Account Id to filter subscriptions

Returns: List<Subscription__c> - Active subscription records

Business Logic (lines 4-9):

try {
    return SubscriptionSelector.getActiveSubscriptions(accountId);
} catch (Exception e) {
    throw new AuraHandledException(e.getMessage());
}

Delegation Pattern: - Controller → SubscriptionSelector (query layer) - No business logic in controller - Exception wrapping for LWC consumption

Issues/Concerns: - ✅ Cacheable: Proper use of cacheable=true for read-only method - ✅ Clean Architecture: Delegates to selector layer - ✅ Exception Handling: Catches all exceptions and wraps for UI - ✅ Final Parameter: Uses final keyword (good practice but optional in Apex)

cancelSubscriptions

@AuraEnabled
public static String cancelSubscriptions(final List<String> subscriptionIds)

Purpose: Cancels multiple subscriptions in a single operation.

Parameters: - subscriptionIds (List) - List of Subscription__c Ids to cancel

Returns: String - Success message

Business Logic (lines 11-20):

try {
    SubscriptionService.cancelSubscriptions(subscriptionIds);
    return SUBSCRIPTIONS_CANCELED_SUCCESSFUL_MESSAGE;
} catch (Exception e) {
    throw new AuraHandledException('Error in cancelSubscriptions: ' + e.getMessage());
    // System.debug('[Controller] Error in cancelSubscriptions: ' + e.getMessage());
    // System.debug('[Controller] Stack trace: ' + e.getStackTraceString());
}

Delegation Pattern: - Controller → SubscriptionService (business logic layer) - Service handles validation, DML, and business rules - Controller only handles exception wrapping and success message

Issues/Concerns: - ⚠️ Commented Debug Statements (lines 17-18): Should be removed - Keep code clean in production - Logging should be in service layer, not controller - ⚠️ Not Cacheable: Correctly marked as NOT cacheable (DML operation) - ✅ Success Message: Returns constant message for UI display - ✅ Error Context: Includes 'Error in cancelSubscriptions:' prefix for debugging - ✅ Service Delegation: Business logic properly isolated in service layer

Dependencies

Custom Objects

  • Subscription__c (Custom Object)
  • Access: Read (via selector), Update (via service)

Other Classes

  • SubscriptionSelector: Query layer for Subscription__c
  • Method: getActiveSubscriptions(String accountId)
  • Returns active subscriptions for account

  • SubscriptionService: Business logic layer for subscriptions

  • Method: cancelSubscriptions(List<String> subscriptionIds)
  • Handles cancellation logic (status updates, validations, etc.)

Design Patterns

  1. Thin Controller Pattern: Controller has no business logic
  2. Service Layer Pattern: Business logic in SubscriptionService
  3. Selector Pattern: Query logic in SubscriptionSelector
  4. Exception Wrapping: All exceptions wrapped in AuraHandledException
  5. Constant Messages: UI messages centralized as constants

Architecture Benefits

✅ Separation of Concerns

  • Controller: LWC integration, exception handling, UI messages
  • Service: Business logic, validations, DML operations
  • Selector: Database queries, SOQL logic

✅ Testability

  • Each layer can be tested independently
  • Service layer tests don't need LWC context
  • Selector tests focus on query logic

✅ Reusability

  • Service methods can be called from controllers, batch classes, triggers
  • Selector methods can be reused across multiple services
  • Not locked into LWC-only usage

✅ Clean Code

  • Controller methods are 3-5 lines each
  • Easy to understand and maintain
  • Consistent error handling pattern

Usage Example

LWC JavaScript (conceptual):

import { LightningElement, wire, api } from 'lwc';
import getActiveSubscriptions from '@salesforce/apex/SubscriptionController.getActiveSubscriptions';
import cancelSubscriptions from '@salesforce/apex/SubscriptionController.cancelSubscriptions';

export default class SubscriptionList extends LightningElement {
    @api accountId;
    subscriptions;
    error;

    @wire(getActiveSubscriptions, { accountId: '$accountId' })
    wiredSubscriptions({ error, data }) {
        if (data) {
            this.subscriptions = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.subscriptions = undefined;
        }
    }

    handleCancel(event) {
        const subscriptionIds = event.detail.selectedIds;
        cancelSubscriptions({ subscriptionIds })
            .then(result => {
                // Show success message: result = 'Selected subscriptions have been successfully cancelled.'
                this.dispatchEvent(new CustomEvent('success', { detail: result }));
            })
            .catch(error => {
                // Show error message
                this.error = error;
            });
    }
}

Security Considerations

Sharing Model

  • WITH SHARING: Respects record-level security
  • Users can only see/cancel subscriptions they have access to
  • SubscriptionSelector and SubscriptionService should also use with sharing

Parameter Validation

  • ⚠️ No Null Checks: Controller assumes parameters are valid
  • Service layer should validate accountId and subscriptionIds
  • Example: if (String.isBlank(accountId)) throw exception;

DML Security

  • Service Layer Responsibility: DML happens in SubscriptionService
  • Service should validate ownership before canceling
  • Service should check FLS (Field-Level Security) if needed

Error Handling

Exception Wrapping

Both methods follow same pattern:

try {
    // Delegate to service/selector
    return result;
} catch (Exception e) {
    throw new AuraHandledException(e.getMessage());
}

Benefits: - LWC receives AuraHandledException (required for proper display) - Original exception message preserved - Stack trace available in debug logs

Considerations: - Catches all exceptions (generic Exception) - Could catch specific exceptions for different error messages - Service layer should throw business-specific exceptions

Test Class Requirements

@IsTest
public class SubscriptionControllerTest {

    @TestSetup
    static void setup() {
        Account acc = TestDataFactory.getAccountRecord(true);
        Product2 prod = TestDataFactory.getProduct(true);
        Subscription__c sub = TestDataFactory.createSubscription(acc.Id, prod.Id);
        sub.Status__c = 'Active';
        insert sub;
    }

    @IsTest
    static void testGetActiveSubscriptions_Success() {
        Account acc = [SELECT Id FROM Account LIMIT 1];

        Test.startTest();
        List<Subscription__c> subs = SubscriptionController.getActiveSubscriptions(acc.Id);
        Test.stopTest();

        Assert.isFalse(subs.isEmpty(), 'Should return active subscriptions');
        Assert.areEqual('Active', subs[0].Status__c, 'Should be active status');
    }

    @IsTest
    static void testGetActiveSubscriptions_NoAccount() {
        Test.startTest();
        try {
            SubscriptionController.getActiveSubscriptions(null);
            Assert.fail('Should throw exception for null accountId');
        } catch (AuraHandledException e) {
            Assert.isTrue(e.getMessage().contains(''), 'Should have error message');
        }
        Test.stopTest();
    }

    @IsTest
    static void testCancelSubscriptions_Success() {
        Subscription__c sub = [SELECT Id FROM Subscription__c LIMIT 1];

        Test.startTest();
        String result = SubscriptionController.cancelSubscriptions(new List<String>{sub.Id});
        Test.stopTest();

        Assert.areEqual('Selected subscriptions have been successfully cancelled.', result, 'Should return success message');

        Subscription__c canceledSub = [SELECT Id, Status__c FROM Subscription__c WHERE Id = :sub.Id];
        Assert.areEqual('Canceled', canceledSub.Status__c, 'Subscription should be canceled');
    }

    @IsTest
    static void testCancelSubscriptions_EmptyList() {
        Test.startTest();
        try {
            SubscriptionController.cancelSubscriptions(new List<String>());
            Assert.fail('Should throw exception for empty list');
        } catch (AuraHandledException e) {
            Assert.isTrue(e.getMessage().contains('Error in cancelSubscriptions'), 'Should have error context');
        }
        Test.stopTest();
    }

    @IsTest
    static void testCancelSubscriptions_InvalidId() {
        Test.startTest();
        try {
            SubscriptionController.cancelSubscriptions(new List<String>{'invalid-id'});
            Assert.fail('Should throw exception for invalid id');
        } catch (AuraHandledException e) {
            Assert.isTrue(e.getMessage().contains('Error in cancelSubscriptions'), 'Should have error context');
        }
        Test.stopTest();
    }
}

Pre-Go-Live Concerns

LOW

  • Commented Debug Code (lines 17-18): Remove commented System.debug statements
  • Keep code clean
  • Spelling Consistency (line 2): "cancelled" vs "canceled"
  • Use American English "canceled" for consistency with Salesforce standard
  • No Parameter Validation: Service layer should validate inputs
  • Document that validation happens in service layer

NONE

  • Architecture: Excellent separation of concerns
  • Exception Handling: Proper AuraHandledException wrapping
  • Sharing Model: Correct use of with sharing
  • Cacheable: Proper use on read-only method

SubscriptionService.cls (conceptual):

public with sharing class SubscriptionService {

    public static void cancelSubscriptions(List<String> subscriptionIds) {
        if (subscriptionIds == null || subscriptionIds.isEmpty()) {
            throw new IllegalArgumentException('Subscription IDs are required');
        }

        List<Subscription__c> subscriptions = [
            SELECT Id, Status__c, Account__c
            FROM Subscription__c
            WHERE Id IN :subscriptionIds
            WITH SECURITY_ENFORCED
        ];

        if (subscriptions.isEmpty()) {
            throw new IllegalArgumentException('No subscriptions found');
        }

        // Business rule: Can only cancel Active or Pending subscriptions
        for (Subscription__c sub : subscriptions) {
            if (sub.Status__c != 'Active' && sub.Status__c != 'Pending') {
                throw new BusinessException('Cannot cancel subscription in status: ' + sub.Status__c);
            }
            sub.Status__c = 'Canceled';
            sub.Canceled_Date__c = Date.today();
        }

        update subscriptions;

        // Optional: Publish platform event, send notification, etc.
    }
}

Changes & History

Date Author Description
Unknown Original Developer Initial implementation with service/selector delegation

Documentation Status: ✅ Complete Code Review Status: ✅ Approved - Excellent architecture, minor cleanup needed Test Coverage: Test class needed Architecture Pattern: Controller → Service → Selector (best practice) LWC Integration: Subscription management components Code Quality: ⭐⭐⭐⭐⭐ Exemplary thin controller pattern