How to keep list of different payment methods in a web account following DDD principles?
Tags: discuss, payment, help, ddd
Published: 2021-08-25 15:44:43

Hello everyone! I really would like to know your opinion about how better handle and keep different payment methods in a single web account. I consider the account as aggregate root in DDD terms and I want keep list of payment methods there. Also account has to have methods such as `makePaymentMethodDefault`, `addPaymentMethod`, `removePaymentMethod` etc. There is business rule saying that there is only one default payment method so when I remove or add new one I have to make other payment methods as not default. Bellow I showed how it looks now for me but I really don't like it. Actually there are other ways to do it, make a service to handle payment methods for account for instance but I want to make everything in account to be sure that all business rules always go correct and keep related items in one place.

Also in system can be a few payment methods:
1. Stripe tokenized card
2. PayPal
3. Something else in a future

Making payment happens in a different module so my question is not about how to charge money or make a payment but about how to keep payment methods.

This is my common payment method interface (TypeScript):
```typescript
export interface IPaymentMethod {
  isDefault(): boolean;

  makeDefault(): void;

  getClass(): new(...args: any[]) => any;

  getPaymentProvider(): PaymentProvider;

  getPaymentMethodType(): PaymentMethodType;

  remove(): void;

  init(owner: IOwningPaymentMethod): void;
}
```

This is my first concrete payment method (TypeScript):
```typescript
export class StripeTokenizedCardPaymentMethod implements IPaymentMethod {
  readonly token: string;
  readonly last4: string;
  readonly expYear: number;
  readonly expMonth: number;
  readonly country: string;
  readonly brand: string;
  private _owner: IOwningPaymentMethod;

  constructor(props: IStripeTokenizedCardPaymentMethodProps) {
    this.token = props.token;
    this.last4 = props.last4;
    this.expYear = props.expYear;
    this.expMonth = props.expMonth;
    this.country = props.country;
    this.brand = props.brand;
  }

  init(owner: IOwningPaymentMethod): void {
    this._owner = owner;
  }

  getClass() {
    return StripeTokenizedCardPaymentMethod;
  }

  getPaymentMethodType(): PaymentMethodType {
    return PaymentMethodType.StripeTokenizedCard;
  }

  getPaymentProvider(): PaymentProvider {
    return PaymentProvider.Stripe;
  }

  isDefault(): boolean {
    return this._owner.getDefaultPaymentMethod().getPaymentProvider() === this.getPaymentProvider()
      && this._owner.getDefaultPaymentMethod().getPaymentMethodType() === this.getPaymentMethodType();
  }

  makeDefault(): void {
    this._owner.makePaymentMethodDefault(this);
  }

  remove(): void {
    this._owner.removePaymentMethod(this);
  }
}
```

And finally aggregate root account (TypeScript):
```typescript
export class Account extends BaseEntity<AccountId> implements IOwningPaymentMethod {
  get updatedAt() {
    return this._updatedAt;
  }

  readonly id: AccountId;
  readonly user: User;
  readonly createdAt: Moment;
  private _updatedAt: Moment;
  private _paymentMethods: IPaymentMethod[] = [];
  private _defaultPaymentMethod: IPaymentMethod;

  constructor(props: IAccountProps) {
    super();
    this.user = props.user;
    this.id = props.id;
    this._updatedAt = props.updatedAt;
    this.createdAt = props.createdAt;
  }

  getDefaultPaymentMethod(): IPaymentMethod {
    return this._defaultPaymentMethod;
  }

  makePaymentMethodDefault(paymentMethod: IPaymentMethod): void {
    this._defaultPaymentMethod = paymentMethod;
  }

  removePaymentMethod(paymentMethod: IPaymentMethod): void {
    throw new Error("Method not implemented.");
  }

  addPaymentMethod(paymentMethod: IPaymentMethod): void {
    paymentMethod.init(this);
    this._paymentMethods.push(paymentMethod);
  }

  static create(props: IAccountProps): Result<Account> {
    const guardResult = Guard.againstNullOrUndefinedBulk([
      { argument: props.user, argumentName: "user" }
    ]);

    if (!guardResult.succeeded) {
      return Result.fail(new Error(guardResult.message));
    }

    const defaultProps: IAccountProps = {
      ...props,
      id: props.id ?? GUID.generateUUID4(),
      createdAt: props.createdAt ?? moment(),
      updatedAt: props.updatedAt ?? moment()
    };

    const account = new Account(defaultProps);

    return Result.ok(account);
  }
}
```