feat(medusa): PaymentCollectionService (#2365)

This commit is contained in:
Carlos R. L. Rodrigues
2022-10-07 04:37:10 -03:00
committed by GitHub
parent edd35631f7
commit d0f274dbe7
5 changed files with 412 additions and 2 deletions

View File

@@ -36,10 +36,10 @@ export enum PaymentCollectionType {
@FeatureFlagEntity(OrderEditingFeatureFlag.key)
export class PaymentCollection extends SoftDeletableEntity {
@DbAwareColumn({ type: "enum", enum: PaymentCollectionType })
type: string
type: PaymentCollectionType
@DbAwareColumn({ type: "enum", enum: PaymentCollectionStatus })
status: string
status: PaymentCollectionStatus
@Column({ nullable: true })
description: string

View File

@@ -0,0 +1,200 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { EventBusService, PaymentCollectionService } from "../index"
import { PaymentCollectionStatus, PaymentCollectionType } from "../../models"
import { EventBusServiceMock } from "../__mocks__/event-bus"
describe("PaymentCollectionService", () => {
afterEach(() => {
jest.clearAllMocks()
})
const paymentCollectionSample = {
id: IdMap.getId("payment-collection-id1"),
region_id: IdMap.getId("region1"),
amount: 100,
created_at: new Date(),
metadata: {
pluginInfo: "xyz",
},
status: PaymentCollectionStatus.NOT_PAID,
}
const paymentCollectionAuthorizedSample = {
id: IdMap.getId("payment-collection-id2"),
region_id: IdMap.getId("region1"),
amount: 35000,
status: PaymentCollectionStatus.AUTHORIZED,
}
const paymentCollectionRepository = MockRepository({
findOne: (query) => {
const map = {
[IdMap.getId("payment-collection-id1")]: paymentCollectionSample,
[IdMap.getId("payment-collection-id2")]:
paymentCollectionAuthorizedSample,
}
if (map[query?.where?.id]) {
return { ...map[query?.where?.id] }
}
return
},
create: (data) => {
return {
...paymentCollectionSample,
...data,
}
},
})
const paymentCollectionService = new PaymentCollectionService({
manager: MockManager,
paymentCollectionRepository,
eventBusService: EventBusServiceMock as unknown as EventBusService,
})
it("should retrieve a payment collection", async () => {
await paymentCollectionService.retrieve(
IdMap.getId("payment-collection-id1")
)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledWith({
where: { id: IdMap.getId("payment-collection-id1") },
})
})
it("should throw error if payment collection is not found", async () => {
const payCol = paymentCollectionService.retrieve(
IdMap.getId("payment-collection-non-existing-id")
)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledTimes(1)
expect(payCol).rejects.toThrow(Error)
})
it("should create a payment collection", async () => {
const entity = await paymentCollectionService.create({
region_id: IdMap.getId("region2"),
type: PaymentCollectionType.ORDER_EDIT,
currency_code: "USD",
amount: 190,
created_by: IdMap.getId("user-id"),
description: "some description",
metadata: {
abc: 123,
},
})
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
id: IdMap.getId("payment-collection-id1"),
region_id: IdMap.getId("region2"),
amount: 190,
created_by: IdMap.getId("user-id"),
status: PaymentCollectionStatus.NOT_PAID,
description: "some description",
metadata: {
abc: 123,
},
})
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
PaymentCollectionService.Events.CREATED,
entity
)
})
it("should update a payment collection with the right arguments", async () => {
const submittedChanges = {
description: "updated description",
status: PaymentCollectionStatus.CAPTURED,
metadata: {
extra: 123,
arr: ["a", "b", "c"],
},
}
const internalChanges = {
...submittedChanges,
}
internalChanges.metadata = {
...internalChanges.metadata,
...{
pluginInfo: "xyz",
},
}
const entity = await paymentCollectionService.update(
IdMap.getId("payment-collection-id1"),
submittedChanges
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.save).toHaveBeenCalledWith(
expect.objectContaining(internalChanges)
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
PaymentCollectionService.Events.UPDATED,
entity
)
})
it("should throw error to update a non-existing payment collection", async () => {
const submittedChanges = {
description: "updated description",
status: PaymentCollectionStatus.CAPTURED,
metadata: {
extra: 123,
arr: ["a", "b", "c"],
},
}
const payCol = paymentCollectionService.update(
IdMap.getId("payment-collection-non-existing"),
submittedChanges
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(0)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
expect(payCol).rejects.toThrow(Error)
})
it("should delete a payment collection", async () => {
const entity = await paymentCollectionService.delete(
IdMap.getId("payment-collection-id1")
)
expect(paymentCollectionRepository.remove).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.remove).toHaveBeenCalledWith(
expect.objectContaining({
id: IdMap.getId("payment-collection-id1"),
region_id: IdMap.getId("region1"),
amount: 100,
})
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
PaymentCollectionService.Events.DELETED,
entity
)
})
it("should ignore to delete a non-existing payment collection", async () => {
const entity = await paymentCollectionService.delete(
IdMap.getId("payment-collection-non-existing")
)
expect(paymentCollectionRepository.remove).toHaveBeenCalledTimes(0)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
expect(entity).toBe(undefined)
})
it("should throw and error when trying to delete an initialized payment collection", async () => {
const entity = paymentCollectionService.delete(
IdMap.getId("payment-collection-id2")
)
expect(paymentCollectionRepository.remove).toHaveBeenCalledTimes(0)
expect(entity).rejects.toThrow(Error)
})
})

View File

@@ -25,6 +25,7 @@ export { default as OrderService } from "./order"
export { default as OrderEditService } from "./order-edit"
export { default as OrderEditItemChangeService } from "./order-edit-item-change"
export { default as PaymentProviderService } from "./payment-provider"
export { default as PaymentCollectionService } from "./payment-collection"
export { default as PricingService } from "./pricing"
export { default as PriceListService } from "./price-list"
export { default as ProductCollectionService } from "./product-collection"

View File

@@ -0,0 +1,163 @@
import { DeepPartial, EntityManager, IsNull } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindConfig } from "../types/common"
import { buildQuery, isDefined, setMetadata } from "../utils"
import { PaymentCollectionRepository } from "../repositories/payment-collection"
import { PaymentCollection, PaymentCollectionStatus } from "../models"
import { TransactionBaseService } from "../interfaces"
import { EventBusService } from "./index"
import { CreatePaymentCollectionInput } from "../types/payment-collection"
type InjectedDependencies = {
manager: EntityManager
paymentCollectionRepository: typeof PaymentCollectionRepository
eventBusService: EventBusService
}
export default class PaymentCollectionService extends TransactionBaseService {
static readonly Events = {
CREATED: "payment-collection.created",
UPDATED: "payment-collection.updated",
DELETED: "payment-collection.deleted",
}
protected readonly manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly eventBusService_: EventBusService
// eslint-disable-next-line max-len
protected readonly paymentCollectionRepository_: typeof PaymentCollectionRepository
constructor({
manager,
paymentCollectionRepository,
eventBusService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.manager_ = manager
this.paymentCollectionRepository_ = paymentCollectionRepository
this.eventBusService_ = eventBusService
}
async retrieve(
paymentCollectionId: string,
config: FindConfig<PaymentCollection> = {}
): Promise<PaymentCollection> {
const manager = this.transactionManager_ ?? this.manager_
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const query = buildQuery({ id: paymentCollectionId }, config)
const paymentCollection = await paymentCollectionRepository.findOne(query)
if (!paymentCollection) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection with id ${paymentCollectionId} was not found`
)
}
return paymentCollection
}
async create(data: CreatePaymentCollectionInput): Promise<PaymentCollection> {
return await this.atomicPhase_(async (transactionManager) => {
const paymentCollectionRepository =
transactionManager.getCustomRepository(
this.paymentCollectionRepository_
)
const paymentCollectionToCreate = paymentCollectionRepository.create({
region_id: data.region_id,
type: data.type,
status: PaymentCollectionStatus.NOT_PAID,
currency_code: data.currency_code,
amount: data.amount,
metadata: data.metadata,
created_by: data.created_by,
description: data.description,
})
const paymentCollection = await paymentCollectionRepository.save(
paymentCollectionToCreate
)
await this.eventBusService_
.withTransaction(transactionManager)
.emit(PaymentCollectionService.Events.CREATED, paymentCollection)
return paymentCollection
})
}
async update(
paymentCollectionId: string,
data: DeepPartial<PaymentCollection>
): Promise<PaymentCollection> {
return await this.atomicPhase_(async (manager) => {
const paymentCollectionRepo = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const paymentCollection = await this.retrieve(paymentCollectionId)
for (const key of Object.keys(data)) {
if (key === "metadata" && data.metadata) {
paymentCollection[key] = setMetadata(paymentCollection, data.metadata)
} else if (isDefined(data[key])) {
paymentCollection[key] = data[key]
}
}
const result = await paymentCollectionRepo.save(paymentCollection)
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.UPDATED, result)
return result
})
}
async delete(
paymentCollectionId: string
): Promise<PaymentCollection | undefined> {
return await this.atomicPhase_(async (manager) => {
const paymentCollectionRepo = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const paymentCollection = await this.retrieve(paymentCollectionId).catch(
() => void 0
)
if (!paymentCollection) {
return
}
if (
[
PaymentCollectionStatus.CANCELED,
PaymentCollectionStatus.NOT_PAID,
].includes(paymentCollection.status) === false
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot delete payment collection with status ${paymentCollection.status}`
)
}
await paymentCollectionRepo.remove(paymentCollection)
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.DELETED, paymentCollection)
return paymentCollection
})
}
}

View File

@@ -0,0 +1,46 @@
import { PaymentCollection, PaymentCollectionType } from "../models"
export type CreatePaymentCollectionInput = {
region_id: string
type: PaymentCollectionType
currency_code: string
amount: number
created_by: string
metadata?: any
description?: string
}
export const defaultPaymentCollectionRelations = [
"region",
"region.payment_providers",
"payment_sessions",
]
export const defaultPaymentCollectionFields: (keyof PaymentCollection)[] = [
"id",
"type",
"status",
"description",
"amount",
"authorized_amount",
"refunded_amount",
"currency_code",
"metadata",
"region",
"payment_sessions",
"payments",
]
// eslint-disable-next-line max-len
export const storePaymentCollectionNotAllowedFieldsAndRelations = ["created_by"]
export const defaultStorePaymentCollectionRelations =
defaultPaymentCollectionRelations.filter(
(field) =>
!storePaymentCollectionNotAllowedFieldsAndRelations.includes(field)
)
export const defaultStorePaymentCollectionFields =
defaultPaymentCollectionFields.filter(
(field) =>
!storePaymentCollectionNotAllowedFieldsAndRelations.includes(field)
)