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:
- Stripe tokenized card
- PayPal
- 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):
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):
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):
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);
}
}
Top comments (4)
You can encapsulate payment in his own ValueObject :
And I thinks your method create is too generic, you can be more explicit :
It looks really interesting and I like your approach to encapsulate logic with payment methods in one object. Thanks!
Hello,
Why you don't like it ?