From 87f14d03a55ddccaf1ce9afd47b79df054774555 Mon Sep 17 00:00:00 2001 From: abdulhakim2902 Date: Thu, 8 Dec 2022 11:42:29 +0700 Subject: [PATCH] fix: seperate content price to model collection --- .../unlockable-content.acceptance.ts | 22 ++++++-- src/__tests__/helpers/database.helper.ts | 18 ++++++- src/__tests__/helpers/given-instances.ts | 20 +++---- src/__tests__/helpers/given-repositories.ts | 7 +++ .../user/unlockable-content.controller.ts | 8 +-- src/models/content-price.model.ts | 45 ++++++++++++++++ src/models/index.ts | 1 + src/models/unlockable-content.model.ts | 35 +++++++----- src/repositories/content-price.repository.ts | 54 +++++++++++++++++++ src/repositories/index.ts | 1 + .../unlockable-content.repository.ts | 21 +++++++- src/services/filter-builder.service.ts | 48 ++++++++--------- src/services/transaction.service.ts | 18 +++---- src/services/user.service.ts | 24 +++++++-- src/services/wallet-address.service.ts | 10 +--- 15 files changed, 255 insertions(+), 77 deletions(-) create mode 100644 src/models/content-price.model.ts create mode 100644 src/repositories/content-price.repository.ts diff --git a/src/__tests__/acceptance/unlockable-content.acceptance.ts b/src/__tests__/acceptance/unlockable-content.acceptance.ts index 5361da890..5d336e00d 100644 --- a/src/__tests__/acceptance/unlockable-content.acceptance.ts +++ b/src/__tests__/acceptance/unlockable-content.acceptance.ts @@ -1,9 +1,11 @@ import {Client, expect} from '@loopback/testlab'; +import {omit} from 'lodash'; import {MyriadApiApplication} from '../../application'; import {ReferenceType} from '../../enums'; import {Token} from '../../interfaces'; import {Currency, UnlockableContentWithRelations, User} from '../../models'; import { + ContentPriceRepository, CurrencyRepository, ServerRepository, TransactionRepository, @@ -13,6 +15,7 @@ import { import { deleteAllRepository, givenAccesToken, + givenContentPriceRepository, givenCurrencyInstance, givenCurrencyRepository, givenOtherUser, @@ -38,6 +41,7 @@ describe('UnlockableContentApplication', () => { let transactionRepository: TransactionRepository; let currencyRepository: CurrencyRepository; let unlockableContentRepository: UnlockableContentRepository; + let contentPriceRepository: ContentPriceRepository; let user: User; let currency: Currency; @@ -53,6 +57,7 @@ describe('UnlockableContentApplication', () => { transactionRepository = await givenTransactionRepository(app); unlockableContentRepository = await givenUnlockableContentRepository(app); serverRepository = await givenServerRepository(app); + contentPriceRepository = await givenContentPriceRepository(app); }); beforeEach(async () => { @@ -72,15 +77,26 @@ describe('UnlockableContentApplication', () => { }); it('create an unlockable content', async () => { - const content = givenUnlockableContent({createdBy: user.id}); + const content = givenUnlockableContent({ + createdBy: user.id, + contentPrices: [{currencyId: currency.id, amount: 100}], + }); const response = await client .post('/user/unlockable-contents') .set('Authorization', `Bearer ${token}`) .send(content) .expect(200); - expect(response.body).to.containDeep(content); + expect(response.body).to.containDeep(omit(content, ['contentPrices'])); const result = await unlockableContentRepository.findById(response.body.id); - expect(result).to.containDeep(content); + expect(result).to.containDeep(omit(content, ['contentPrices'])); + const contentPrice = await contentPriceRepository.findOne({ + where: {unlockableContentId: result.id}, + }); + expect(contentPrice).to.containEql({ + currencyId: currency.id, + amount: 100, + unlockableContentId: result.id, + }); }); context('when dealing with a single persisted unlockable content', () => { diff --git a/src/__tests__/helpers/database.helper.ts b/src/__tests__/helpers/database.helper.ts index 464370044..d684e16b7 100644 --- a/src/__tests__/helpers/database.helper.ts +++ b/src/__tests__/helpers/database.helper.ts @@ -33,6 +33,7 @@ import { UserOTPRepository, IdentityRepository, UnlockableContentRepository, + ContentPriceRepository, } from '../../repositories'; import { ActivityLogService, @@ -103,8 +104,18 @@ export async function givenRepositories(testdb: any) { async () => postRepository, async () => userRepository, ); + const contentPriceRepository: ContentPriceRepository = + new ContentPriceRepository( + testdb, + async () => currencyRepository, + async () => unlockableContentRepository, + ); const unlockableContentRepository: UnlockableContentRepository = - new UnlockableContentRepository(testdb, async () => userRepository); + new UnlockableContentRepository( + testdb, + async () => userRepository, + async () => contentPriceRepository, + ); const postRepository: PostRepository = new PostRepository( testdb, async () => peopleRepository, @@ -321,6 +332,7 @@ export async function givenRepositories(testdb: any) { ); const transactionService = new TransactionService( + contentPriceRepository, currencyRepository, peopleRepository, transactionRepository, @@ -415,6 +427,7 @@ export async function givenRepositories(testdb: any) { ); const userService = new UserService( + contentPriceRepository, changeEmailRequestRepository, experienceRepository, identityRepository, @@ -484,6 +497,7 @@ export async function givenRepositories(testdb: any) { socialMediaService, tagService, unlockableContentRepository, + contentPriceRepository, }; } @@ -513,6 +527,7 @@ export async function givenEmptyDatabase(testdb: any) { userCurrencyRepository, serverRepository, unlockableContentRepository, + contentPriceRepository, } = await givenRepositories(testdb); await tagRepository.deleteAll(); @@ -539,4 +554,5 @@ export async function givenEmptyDatabase(testdb: any) { await userCurrencyRepository.deleteAll(); await serverRepository.deleteAll(); await unlockableContentRepository.deleteAll(); + await contentPriceRepository.deleteAll(); } diff --git a/src/__tests__/helpers/given-instances.ts b/src/__tests__/helpers/given-instances.ts index b72cbf6be..9de064f13 100644 --- a/src/__tests__/helpers/given-instances.ts +++ b/src/__tests__/helpers/given-instances.ts @@ -32,7 +32,7 @@ import { SocialMediaVerificationDto, Tag, Transaction, - UnlockableContent, + UnlockableContentWithPrice, User, UserExperience, UserReport, @@ -892,28 +892,24 @@ export function givenIdentityInstance( } export function givenUnlockableContent( - unlockableContent?: Partial, + unlockableContentWithPrice?: Partial, ) { const data = Object.assign( { content: { text: 'Hello world', }, - prices: [ - { - ...givenCurrency(), - amount: 100, - }, - ], }, - unlockableContent, + unlockableContentWithPrice, ); - return new UnlockableContent(data); + return new UnlockableContentWithPrice(data); } export function givenUnlockableContentInstance( unlockableRepository: UnlockableContentRepository, - unlockableContent?: Partial, + unlockableContentWithPrice?: Partial, ) { - return unlockableRepository.create(givenUnlockableContent(unlockableContent)); + return unlockableRepository.create( + givenUnlockableContent(unlockableContentWithPrice), + ); } diff --git a/src/__tests__/helpers/given-repositories.ts b/src/__tests__/helpers/given-repositories.ts index 38b068cbe..d6aca79ea 100644 --- a/src/__tests__/helpers/given-repositories.ts +++ b/src/__tests__/helpers/given-repositories.ts @@ -26,6 +26,7 @@ import { ServerRepository, IdentityRepository, UnlockableContentRepository, + ContentPriceRepository, } from '../../repositories'; export async function givenUserRepository(app: MyriadApiApplication) { @@ -138,6 +139,10 @@ export async function givenUnlockableContentRepository( return app.getRepository(UnlockableContentRepository); } +export async function givenContentPriceRepository(app: MyriadApiApplication) { + return app.getRepository(ContentPriceRepository); +} + export async function deleteAllRepository(app: MyriadApiApplication) { const userRepository = await givenUserRepository(app); const friendRepository = await givenFriendRepository(app); @@ -168,6 +173,7 @@ export async function deleteAllRepository(app: MyriadApiApplication) { const unlockableContentRepository = await givenUnlockableContentRepository( app, ); + const contentPriceRepository = await givenContentPriceRepository(app); await userRepository.deleteAll(); await friendRepository.deleteAll(); @@ -196,4 +202,5 @@ export async function deleteAllRepository(app: MyriadApiApplication) { await serverRepository.deleteAll(); await identityRepository.deleteAll(); await unlockableContentRepository.deleteAll(); + await contentPriceRepository.deleteAll(); } diff --git a/src/controllers/user/unlockable-content.controller.ts b/src/controllers/user/unlockable-content.controller.ts index 49c03e6ed..3b49ede20 100644 --- a/src/controllers/user/unlockable-content.controller.ts +++ b/src/controllers/user/unlockable-content.controller.ts @@ -16,7 +16,7 @@ import { response, } from '@loopback/rest'; import {PaginationInterceptor} from '../../interceptors'; -import {UnlockableContent} from '../../models'; +import {UnlockableContent, UnlockableContentWithPrice} from '../../models'; import {UserService} from '../../services'; @authenticate('jwt') @@ -30,21 +30,21 @@ export class UserUnlockableContentController { @response(200, { description: 'CREATE user unlockable-content', 'application/json': { - schema: getModelSchemaRef(UnlockableContent), + schema: getModelSchemaRef(UnlockableContentWithPrice), }, }) async create( @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(UnlockableContent, { + schema: getModelSchemaRef(UnlockableContentWithPrice, { title: 'NewUnlockableContent', exclude: ['id'], }), }, }, }) - content: Omit, + content: Omit, ): Promise { return this.userService.createUnlockableContent(content); } diff --git a/src/models/content-price.model.ts b/src/models/content-price.model.ts new file mode 100644 index 000000000..0b20c2686 --- /dev/null +++ b/src/models/content-price.model.ts @@ -0,0 +1,45 @@ +import {Entity, model, property, belongsTo} from '@loopback/repository'; +import {Currency} from './currency.model'; +import {UnlockableContent} from './unlockable-content.model'; + +@model({ + settings: { + strictObjectIDCoercion: true, + mongodb: { + collection: 'contentPrices', + }, + }, +}) +export class ContentPrice extends Entity { + @property({ + type: 'string', + id: true, + generated: true, + mongodb: { + dataType: 'ObjectId', + }, + }) + id: string; + + @property({ + type: 'number', + required: true, + }) + amount: number; + + @belongsTo(() => Currency) + currencyId: string; + + @belongsTo(() => UnlockableContent) + unlockableContentId: string; + + constructor(data?: Partial) { + super(data); + } +} + +export interface ContentPriceRelations { + // describe navigational properties here +} + +export type ContentPriceWithRelations = ContentPrice & ContentPriceRelations; diff --git a/src/models/index.ts b/src/models/index.ts index 5b95dc630..9c11df3e8 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -46,3 +46,4 @@ export * from './user-personal-access-token.model'; export * from './user.model'; export * from './vote.model'; export * from './wallet.model'; +export * from './content-price.model'; diff --git a/src/models/unlockable-content.model.ts b/src/models/unlockable-content.model.ts index f57b31522..9a74f42d0 100644 --- a/src/models/unlockable-content.model.ts +++ b/src/models/unlockable-content.model.ts @@ -4,13 +4,10 @@ import { model, property, belongsTo, + hasMany, } from '@loopback/repository'; -import {Currency} from './currency.model'; import {User, UserWithRelations} from './user.model'; - -type Price = Currency & { - amount: string; -}; +import {ContentPrice} from './content-price.model'; @model({ settings: { @@ -46,13 +43,6 @@ export class UnlockableContent extends Entity { }) content?: AnyObject; - @property({ - type: 'array', - itemType: 'object', - required: true, - }) - prices: Price[]; - @property({ type: 'date', required: false, @@ -73,6 +63,9 @@ export class UnlockableContent extends Entity { }) deletedAt?: string; + @hasMany(() => ContentPrice) + prices: ContentPrice[]; + @belongsTo(() => User, {name: 'user'}) createdBy: string; @@ -88,3 +81,21 @@ export interface UnlockableContentRelations { export type UnlockableContentWithRelations = UnlockableContent & UnlockableContentRelations; + +export interface Price { + currencyId: string; + amount: number; +} + +export class UnlockableContentWithPrice extends UnlockableContent { + @property({ + type: 'array', + itemType: 'object', + required: false, + }) + contentPrices: Price[]; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/src/repositories/content-price.repository.ts b/src/repositories/content-price.repository.ts new file mode 100644 index 000000000..34db6cf49 --- /dev/null +++ b/src/repositories/content-price.repository.ts @@ -0,0 +1,54 @@ +import {inject, Getter} from '@loopback/core'; +import { + DefaultCrudRepository, + repository, + BelongsToAccessor, +} from '@loopback/repository'; +import {MongoDataSource} from '../datasources'; +import { + ContentPrice, + ContentPriceRelations, + Currency, + UnlockableContent, +} from '../models'; +import {CurrencyRepository} from './currency.repository'; +import {UnlockableContentRepository} from './unlockable-content.repository'; + +export class ContentPriceRepository extends DefaultCrudRepository< + ContentPrice, + typeof ContentPrice.prototype.id, + ContentPriceRelations +> { + public readonly currency: BelongsToAccessor< + Currency, + typeof ContentPrice.prototype.id + >; + + public readonly unlockableContent: BelongsToAccessor< + UnlockableContent, + typeof ContentPrice.prototype.id + >; + + constructor( + @inject('datasources.mongo') dataSource: MongoDataSource, + @repository.getter('CurrencyRepository') + protected currencyRepositoryGetter: Getter, + @repository.getter('UnlockableContentRepository') + protected unlockableContentRepositoryGetter: Getter, + ) { + super(ContentPrice, dataSource); + this.unlockableContent = this.createBelongsToAccessorFor( + 'unlockableContent', + unlockableContentRepositoryGetter, + ); + this.registerInclusionResolver( + 'unlockableContent', + this.unlockableContent.inclusionResolver, + ); + this.currency = this.createBelongsToAccessorFor( + 'currency', + currencyRepositoryGetter, + ); + this.registerInclusionResolver('currency', this.currency.inclusionResolver); + } +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts index 4d5e7fc6a..564be7d41 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -34,3 +34,4 @@ export * from './user-otp.repository'; export * from './user-personal-access-token.repository'; export * from './change-email-request.repository'; export * from './request-create-new-user-by-email.repository'; +export * from './content-price.repository'; diff --git a/src/repositories/unlockable-content.repository.ts b/src/repositories/unlockable-content.repository.ts index 30d67c749..53ea5ebf5 100644 --- a/src/repositories/unlockable-content.repository.ts +++ b/src/repositories/unlockable-content.repository.ts @@ -3,10 +3,17 @@ import { DefaultCrudRepository, repository, BelongsToAccessor, + HasManyRepositoryFactory, } from '@loopback/repository'; import {MongoDataSource} from '../datasources'; -import {UnlockableContent, UnlockableContentRelations, User} from '../models'; +import { + UnlockableContent, + UnlockableContentRelations, + User, + ContentPrice, +} from '../models'; import {UserRepository} from './user.repository'; +import {ContentPriceRepository} from './content-price.repository'; export class UnlockableContentRepository extends DefaultCrudRepository< UnlockableContent, @@ -18,12 +25,24 @@ export class UnlockableContentRepository extends DefaultCrudRepository< typeof UnlockableContent.prototype.id >; + public readonly prices: HasManyRepositoryFactory< + ContentPrice, + typeof UnlockableContent.prototype.id + >; + constructor( @inject('datasources.mongo') dataSource: MongoDataSource, @repository.getter('UserRepository') protected userRepositoryGetter: Getter, + @repository.getter('ContentPriceRepository') + protected contentPriceRepositoryGetter: Getter, ) { super(UnlockableContent, dataSource); + this.prices = this.createHasManyRepositoryFactoryFor( + 'prices', + contentPriceRepositoryGetter, + ); + this.registerInclusionResolver('prices', this.prices.inclusionResolver); this.user = this.createBelongsToAccessorFor('user', userRepositoryGetter); this.registerInclusionResolver('user', this.user.inclusionResolver); } diff --git a/src/services/filter-builder.service.ts b/src/services/filter-builder.service.ts index 338b96ea6..38f644478 100644 --- a/src/services/filter-builder.service.ts +++ b/src/services/filter-builder.service.ts @@ -203,30 +203,6 @@ export class FilterBuilderService { return this.finalizeFilter(filter, {userId, networkId}); } - public async userExperienceById(args: InvocationArgs): Promise { - const filter = args[1] ?? {}; - const include = [ - { - relation: 'experience', - scope: { - include: [ - { - relation: 'user', - scope: { - include: [{relation: 'accountSetting'}], - }, - }, - ], - }, - }, - ]; - - if (!filter.include) filter.include = include; - else filter.include.push(...include); - - args[1] = filter; - } - public async userFriend( filter: Filter, query: Query, @@ -348,6 +324,30 @@ export class FilterBuilderService { return this.finalizeFilter(filter, where); } + public async userExperienceById(args: InvocationArgs): Promise { + const filter = args[1] ?? {}; + const include = [ + { + relation: 'experience', + scope: { + include: [ + { + relation: 'user', + scope: { + include: [{relation: 'accountSetting'}], + }, + }, + ], + }, + }, + ]; + + if (!filter.include) filter.include = include; + else filter.include.push(...include); + + args[1] = filter; + } + public async userPostById(args: InvocationArgs): Promise { const filter = args[1] ?? {}; const experienceFilter = { diff --git a/src/services/transaction.service.ts b/src/services/transaction.service.ts index 8835b7ab6..bce483887 100644 --- a/src/services/transaction.service.ts +++ b/src/services/transaction.service.ts @@ -13,10 +13,10 @@ import { WalletWithRelations, } from '../models'; import { + ContentPriceRepository, CurrencyRepository, PeopleRepository, TransactionRepository, - UnlockableContentRepository, UserRepository, UserSocialMediaRepository, WalletRepository, @@ -30,14 +30,14 @@ import {u8aToHex} from '@polkadot/util'; @injectable({scope: BindingScope.TRANSIENT}) export class TransactionService { constructor( + @repository(ContentPriceRepository) + private contentPriceRepository: ContentPriceRepository, @repository(CurrencyRepository) private currencyRepository: CurrencyRepository, @repository(PeopleRepository) private peopleRepository: PeopleRepository, @repository(TransactionRepository) private transactionRepository: TransactionRepository, - @repository(UnlockableContentRepository) - private unlockableContentRepository: UnlockableContentRepository, @repository(UserRepository) private userRepository: UserRepository, @repository(UserSocialMediaRepository) @@ -202,15 +202,15 @@ export class TransactionService { let methodName; if (transaction.type === ReferenceType.UNLOCKABLECONTENT) { - const id = transaction.referenceId ?? ''; - const content = await this.unlockableContentRepository.findById(id); - const price = content.prices.find(e => e.id === transaction.currencyId); + const price = await this.contentPriceRepository.findOne({ + where: {unlockableContentId: transaction.referenceId}, + }); if (!price) { throw new HttpErrors.NotFound('ContentPriceNotFound'); } - if (transaction.amount < parseInt(price.amount)) { + if (transaction.amount < price.amount) { throw new HttpErrors.NotFound('InvalidPayment'); } @@ -296,11 +296,11 @@ export class TransactionService { ) { const {referenceType, referenceId} = tipsBalanceInfo; if (referenceType !== transaction.type) { - throw new HttpErrors.UnprocessableEntity('ContentNotPaid'); + throw new HttpErrors.UnprocessableEntity('InvalidReference'); } if (referenceId !== transaction.referenceId) { - throw new HttpErrors.UnprocessableEntity('ContentNotPaid'); + throw new HttpErrors.UnprocessableEntity('InvalidReference'); } } } diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 1574e160a..0afb33252 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -12,7 +12,7 @@ import {HttpErrors} from '@loopback/rest'; import {securityId, UserProfile} from '@loopback/security'; import {isHex} from '@polkadot/util'; import NonceGenerator from 'a-nonce-generator'; -import {assign, omit, pull, union} from 'lodash'; +import {assign, omit, pull, union, uniqBy} from 'lodash'; import {config} from '../config'; import { AccountSettingType, @@ -27,6 +27,7 @@ import { AccountSetting, ActivityLog, Comment, + ContentPrice, CreateImportedPostDto, CreateReportDto, CreateUserPersonalAccessTokenDto, @@ -46,6 +47,7 @@ import { Transaction, TxDetail, UnlockableContent, + UnlockableContentWithPrice, UpdateTransactionDto, UpdateUserDto, UpdateUserPersonalAccessTokenDto, @@ -59,6 +61,7 @@ import { } from '../models'; import { ChangeEmailRequestRepository, + ContentPriceRepository, ExperienceRepository, IdentityRepository, UnlockableContentRepository, @@ -93,6 +96,8 @@ export interface AfterFindProps { @injectable({scope: BindingScope.TRANSIENT}) export class UserService { constructor( + @repository(ContentPriceRepository) + private contentPriceRepository: ContentPriceRepository, @repository(ChangeEmailRequestRepository) private changeEmailRequestRepository: ChangeEmailRequestRepository, @repository(ExperienceRepository) @@ -904,10 +909,23 @@ export class UserService { // ------ UnlockableContentMethod ----------------- public async createUnlockableContent( - content: Omit, + content: Omit, ): Promise { content.createdBy = this.currentUser[securityId]; - return this.unlockableContentRepository.create(content); + const raw = omit(content, 'contentPrices'); + const created = await this.unlockableContentRepository.create(raw); + if (content.contentPrices.length === 0) return created; + const prices = uniqBy(content.contentPrices, 'id').map(price => { + const contentPrice = new ContentPrice(); + contentPrice.amount = price.amount; + contentPrice.unlockableContentId = created.id; + contentPrice.currencyId = price.currencyId; + return contentPrice; + }); + + await this.contentPriceRepository.createAll(prices); + + return created; } public async unlockableContents(filter?: Filter) { diff --git a/src/services/wallet-address.service.ts b/src/services/wallet-address.service.ts index ef916d80f..cc47e926d 100644 --- a/src/services/wallet-address.service.ts +++ b/src/services/wallet-address.service.ts @@ -179,14 +179,8 @@ export class WalletAddressService { return this.tipsBalanceInfo(networkId, ReferenceType.USER, id); } - private async unlockableContentAddress( - id: string, - networkId?: string, - ): Promise { - const {networkId: current, networkIds} = await this.currentUserNetwork(); - if (current !== networkId) { - throw new HttpErrors.UnprocessableEntity('NetworkNotMatch'); - } + private async unlockableContentAddress(id: string): Promise { + const {networkId, networkIds} = await this.currentUserNetwork(); const unlockableContent = await this.unlockableContentRepository.findById( id, {