Skip to content

Commit 2a54638

Browse files
lengyel-arpad85njlie
authored andcommitted
feat(pos): merchants services (#3528)
* merchant service * jest setup * tests
1 parent 705d7de commit 2a54638

File tree

6 files changed

+250
-0
lines changed

6 files changed

+250
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { knex } from 'knex'
2+
import { GenericContainer, Wait } from 'testcontainers'
3+
require('./jest.env') // set environment variables
4+
5+
const POSTGRES_PORT = 5432
6+
7+
const setup = async (globalConfig): Promise<void> => {
8+
const workers = globalConfig.maxWorkers
9+
10+
const setupDatabase = async () => {
11+
if (!process.env.DATABASE_URL) {
12+
const postgresContainer = await new GenericContainer('postgres:15')
13+
.withExposedPorts(POSTGRES_PORT)
14+
.withBindMounts([
15+
{
16+
source: __dirname + '/scripts/init.sh',
17+
target: '/docker-entrypoint-initdb.d/init.sh'
18+
}
19+
])
20+
.withEnvironment({
21+
POSTGRES_PASSWORD: 'password'
22+
})
23+
.withHealthCheck({
24+
test: ['CMD-SHELL', 'pg_isready -d testing'],
25+
interval: 10000,
26+
timeout: 5000,
27+
retries: 5
28+
})
29+
.withWaitStrategy(Wait.forHealthCheck())
30+
.start()
31+
32+
process.env.DATABASE_URL = `postgresql://postgres:password@localhost:${postgresContainer.getMappedPort(
33+
POSTGRES_PORT
34+
)}/testing`
35+
36+
global.__POS_POSTGRES__ = postgresContainer
37+
}
38+
39+
const db = knex({
40+
client: 'postgresql',
41+
connection: process.env.DATABASE_URL,
42+
pool: {
43+
min: 2,
44+
max: 10
45+
},
46+
migrations: {
47+
tableName: 'pos_knex_migrations'
48+
}
49+
})
50+
51+
// node pg defaults to returning bigint as string. This ensures it parses to bigint
52+
db.client.driver.types.setTypeParser(
53+
db.client.driver.types.builtins.INT8,
54+
'text',
55+
BigInt
56+
)
57+
await db.migrate.latest({
58+
directory: __dirname + '/migrations'
59+
})
60+
61+
for (let i = 1; i <= workers; i++) {
62+
const workerDatabaseName = `testing_${i}`
63+
64+
await db.raw(`DROP DATABASE IF EXISTS ${workerDatabaseName}`)
65+
await db.raw(`CREATE DATABASE ${workerDatabaseName} TEMPLATE testing`)
66+
}
67+
68+
global.__POS_KNEX__ = db
69+
}
70+
71+
await Promise.all([setupDatabase()])
72+
}
73+
74+
export default setup
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = async () => {
2+
await global.__POS_KNEX__.migrate.rollback(
3+
{ directory: __dirname + '/migrations' },
4+
true
5+
)
6+
await global.__POS_KNEX__.destroy()
7+
if (global.__POS_POSTGRES__) {
8+
await global.__POS_POSTGRES__.stop()
9+
}
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
set -e
3+
4+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
5+
DROP DATABASE IF EXISTS TESTING;
6+
CREATE DATABASE testing;
7+
CREATE DATABASE development;
8+
EOSQL
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { IocContract } from '@adonisjs/fold'
2+
3+
import { Merchant } from './model'
4+
import { MerchantService } from './service'
5+
6+
import { createTestApp, TestContainer } from '../tests/app'
7+
import { truncateTables } from '../tests/tableManager'
8+
import { Config } from '../config/app'
9+
10+
import { initIocContainer } from '../'
11+
import { AppServices } from '../app'
12+
13+
describe('Merchant Service', (): void => {
14+
let deps: IocContract<AppServices>
15+
let appContainer: TestContainer
16+
let merchantService: MerchantService
17+
18+
beforeAll(async (): Promise<void> => {
19+
deps = initIocContainer({
20+
...Config
21+
})
22+
23+
appContainer = await createTestApp(deps)
24+
merchantService = await deps.use('merchantService')
25+
})
26+
27+
afterEach(async (): Promise<void> => {
28+
jest.useRealTimers()
29+
await truncateTables(deps)
30+
})
31+
32+
afterAll(async (): Promise<void> => {
33+
await appContainer.shutdown()
34+
})
35+
36+
describe('create', (): void => {
37+
test('creates a merchant', async (): Promise<void> => {
38+
const merchant = await merchantService.create('Test merchant')
39+
expect(merchant).toEqual({ id: merchant.id, name: 'Test merchant' })
40+
})
41+
})
42+
43+
describe('delete', (): void => {
44+
test('soft deletes an existing merchant', async (): Promise<void> => {
45+
const created = await merchantService.create('Test merchant')
46+
47+
const result = await merchantService.delete(created.id)
48+
expect(result).toBe(true)
49+
50+
const deletedMerchant = await Merchant.query()
51+
.findById(created.id)
52+
.whereNotNull('deletedAt')
53+
expect(deletedMerchant).toBeDefined()
54+
expect(deletedMerchant?.deletedAt).toBeDefined()
55+
})
56+
57+
test('returns false for already deleted merchant', async (): Promise<void> => {
58+
const created = await merchantService.create('Test merchant')
59+
60+
await merchantService.delete(created.id)
61+
const secondDelete = await merchantService.delete(created.id)
62+
expect(secondDelete).toBe(false)
63+
})
64+
})
65+
})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { BaseService } from '../shared/baseService'
2+
import { TransactionOrKnex } from 'objection'
3+
import { Merchant } from './model'
4+
5+
export interface MerchantService {
6+
create(name: string): Promise<Merchant>
7+
delete(id: string): Promise<boolean>
8+
}
9+
10+
interface ServiceDependencies extends BaseService {
11+
knex: TransactionOrKnex
12+
}
13+
14+
export async function createMerchantService({
15+
logger,
16+
knex
17+
}: ServiceDependencies): Promise<MerchantService> {
18+
const log = logger.child({
19+
service: 'MerchantService'
20+
})
21+
const deps: ServiceDependencies = {
22+
logger: log,
23+
knex
24+
}
25+
26+
return {
27+
create: (input: string) => createMerchant(deps, input),
28+
delete: (id: string) => deleteMerchant(deps, id)
29+
}
30+
}
31+
32+
async function createMerchant(
33+
deps: ServiceDependencies,
34+
name: string
35+
): Promise<Merchant> {
36+
return await Merchant.query(deps.knex).insert({ name })
37+
}
38+
39+
async function deleteMerchant(
40+
deps: ServiceDependencies,
41+
id: string
42+
): Promise<boolean> {
43+
const deleted = await Merchant.query(deps.knex)
44+
.patch({ deletedAt: new Date() })
45+
.whereNull('deletedAt')
46+
.where('id', id)
47+
return deleted > 0
48+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { IocContract } from '@adonisjs/fold'
2+
import { Knex } from 'knex'
3+
import { AppServices } from '../app'
4+
5+
export async function truncateTable(
6+
knex: Knex,
7+
tableName: string
8+
): Promise<void> {
9+
const RAW = `TRUNCATE TABLE "${tableName}" RESTART IDENTITY`
10+
await knex.raw(RAW)
11+
}
12+
13+
export async function truncateTables(
14+
deps: IocContract<AppServices>
15+
): Promise<void> {
16+
const knex = await deps.use('knex')
17+
const config = await deps.use('config')
18+
const dbSchema = config.dbSchema ?? 'public'
19+
20+
const ignoreTables = [
21+
'knex_migrations',
22+
'knex_migrations_lock',
23+
'pos_knex_migrations',
24+
'pos_knex_migrations_lock'
25+
]
26+
const tables = await getTables(knex, dbSchema, ignoreTables)
27+
const RAW = `TRUNCATE TABLE "${tables}" RESTART IDENTITY`
28+
await knex.raw(RAW)
29+
}
30+
31+
async function getTables(
32+
knex: Knex,
33+
dbSchema: string = 'public',
34+
ignoredTables: string[]
35+
): Promise<string> {
36+
const result = await knex.raw(
37+
`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='${dbSchema}'`
38+
)
39+
return result.rows
40+
.map((val: { tablename: string }) => {
41+
if (!ignoredTables.includes(val.tablename)) return val.tablename
42+
})
43+
.filter(Boolean)
44+
.join('","')
45+
}

0 commit comments

Comments
 (0)