-
Notifications
You must be signed in to change notification settings - Fork 102
feat(card-service): card payments table #3514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
lengyel-arpad85
merged 14 commits into
pos-card-services
from
al/raf-1094-card-payments-table
Jul 8, 2025
Merged
Changes from 8 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
5c51cab
packages
lengyel-arpad85 1387a80
shared
lengyel-arpad85 93638a7
migration & model for card payments
lengyel-arpad85 8f6ec17
format
lengyel-arpad85 c64e0ad
cleanup
lengyel-arpad85 2f0f94e
prefix knex migrations table
lengyel-arpad85 70128e2
fix naming
lengyel-arpad85 dd1f81c
unused
lengyel-arpad85 7973221
updated migration
lengyel-arpad85 a5d5d43
remove unused
lengyel-arpad85 6e2b5af
lockfile fix
lengyel-arpad85 f323018
unused package removed
lengyel-arpad85 859f7b1
lockfile
lengyel-arpad85 10cb96d
name corrected
lengyel-arpad85 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
packages/card-service/migrations/20250708070327_card_payments_table.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /** | ||
| * @param { import("knex").Knex } knex | ||
| * @returns { Promise<void> } | ||
| */ | ||
| exports.up = function (knex) { | ||
| return knex.schema.createTable('cardPayments', function (table) { | ||
| table.uuid('id').notNullable().primary() | ||
| table.uuid('requestId').notNullable() | ||
|
|
||
| table.timestamp('requestedAt').defaultTo(knex.fn.now()) | ||
| table.timestamp('finalizedAt').defaultTo(knex.fn.now()) | ||
|
|
||
| table.string('cardWalletAddress').notNullable() | ||
| table.string('incomingPaymentUrl').notNullable() | ||
|
|
||
| table.integer('statusCode').nullable() | ||
|
|
||
| table.uuid('outgoingPaymentId') | ||
| table.uuid('terminalId') | ||
|
|
||
| table.index('cardWalletAddress') | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * @param { import("knex").Knex } knex | ||
| * @returns { Promise<void> } | ||
| */ | ||
| exports.down = function (knex) { | ||
| return knex.schema.dropTableIfExists('cardPayments') | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { BaseModel } from '../shared/baseModel' | ||
|
|
||
| export class CardPayments extends BaseModel { | ||
lengyel-arpad85 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| public static get tableName(): string { | ||
| return 'card_payments' | ||
| } | ||
|
|
||
| public requestId!: string | ||
| public requestedAt!: Date | null | ||
| public finalizedAt!: Date | null | ||
| public cardWalletAddress!: string | ||
| public incomingPaymentUrl?: string | ||
| public statusCode?: number | ||
| public outgoingPaymentId?: string | ||
| public terminalId!: string | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { BaseModel, Pagination, SortOrder } from './baseModel' | ||
| import { getPageInfo } from './pagination' | ||
|
|
||
| interface PageTestsOptions<Type> { | ||
| createModel: () => Promise<Type> | ||
| getPage: (pagination?: Pagination, sortOrder?: SortOrder) => Promise<Type[]> | ||
| } | ||
|
|
||
| export const getPageTests = <Type extends BaseModel>({ | ||
| createModel, | ||
| getPage | ||
| }: PageTestsOptions<Type>): void => { | ||
| describe('Common BaseModel pagination', (): void => { | ||
| let modelsCreated: Type[] | ||
|
|
||
| beforeEach(async (): Promise<void> => { | ||
| modelsCreated = [] | ||
| for (let i = 0; i < 22; i++) { | ||
| modelsCreated.push(await createModel()) | ||
| } | ||
| modelsCreated.reverse() // default sort order is DESC | ||
| }) | ||
|
|
||
| test.each` | ||
| pagination | expected | description | ||
| ${undefined} | ${{ length: 20, first: 0, last: 19 }} | ${'Defaults to fetching first 20 items'} | ||
| ${{ first: 10 }} | ${{ length: 10, first: 0, last: 9 }} | ${'Can change forward pagination limit'} | ||
| ${{ after: 0 }} | ${{ length: 20, first: 1, last: 20 }} | ${'Can paginate forwards from a cursor'} | ||
| ${{ first: 10, after: 9 }} | ${{ length: 10, first: 10, last: 19 }} | ${'Can paginate forwards from a cursor with a limit'} | ||
| ${{ before: 20 }} | ${{ length: 20, first: 0, last: 19 }} | ${'Can paginate backwards from a cursor'} | ||
| ${{ last: 5, before: 10 }} | ${{ length: 5, first: 5, last: 9 }} | ${'Can paginate backwards from a cursor with a limit'} | ||
| ${{ after: 0, before: 19 }} | ${{ length: 20, first: 1, last: 20 }} | ${'Providing before and after results in forward pagination'} | ||
| `('$description', async ({ pagination, expected }): Promise<void> => { | ||
| if (pagination?.after !== undefined) { | ||
| pagination.after = modelsCreated[pagination.after].id | ||
| } | ||
| if (pagination?.before !== undefined) { | ||
| pagination.before = modelsCreated[pagination.before].id | ||
| } | ||
| const models = await getPage(pagination) | ||
| expect(models).toHaveLength(expected.length) | ||
| expect(models[0].id).toEqual(modelsCreated[expected.first].id) | ||
| expect(models[expected.length - 1].id).toEqual( | ||
| modelsCreated[expected.last].id | ||
| ) | ||
| }) | ||
|
|
||
| test.each` | ||
| pagination | expectedError | description | ||
| ${{ last: 10 }} | ${"Can't paginate backwards from the start."} | ${"Can't change backward pagination limit on it's own."} | ||
| ${{ first: -1 }} | ${'Pagination index error'} | ${"Can't request less than 0"} | ||
| ${{ first: 101 }} | ${'Pagination index error'} | ${"Can't request more than 100"} | ||
| `('$description', async ({ pagination, expectedError }): Promise<void> => { | ||
| await expect(getPage(pagination)).rejects.toThrow(expectedError) | ||
| }) | ||
|
|
||
| test.each` | ||
| order | description | ||
| ${SortOrder.Asc} | ${'Backwards/Forwards pagination results in same order for ASC.'} | ||
| ${SortOrder.Desc} | ${'Backwards/Forwards pagination results in same order for DESC.'} | ||
| `('$description', async ({ order }): Promise<void> => { | ||
| if (order === SortOrder.Asc) { | ||
| // model was in DESC order so needs to be reverted back to ASC | ||
| modelsCreated.reverse() | ||
| } | ||
| const paginationForwards = { | ||
| first: 10 | ||
| } | ||
| const modelsForwards = await getPage(paginationForwards, order) | ||
| const paginationBackwards = { | ||
| last: 10, | ||
| before: modelsCreated[10].id | ||
| } | ||
| const modelsBackwards = await getPage(paginationBackwards, order) | ||
| expect(modelsForwards).toHaveLength(10) | ||
| expect(modelsBackwards).toHaveLength(10) | ||
| expect(modelsForwards).toEqual(modelsBackwards) | ||
| }) | ||
|
|
||
| test.each` | ||
| pagination | cursor | start | end | hasNextPage | hasPreviousPage | sortOrder | ||
| ${null} | ${null} | ${0} | ${19} | ${true} | ${false} | ${SortOrder.Desc} | ||
| ${{ first: 5 }} | ${null} | ${0} | ${4} | ${true} | ${false} | ${SortOrder.Desc} | ||
| ${{ first: 22 }} | ${null} | ${0} | ${21} | ${false} | ${false} | ${SortOrder.Desc} | ||
| ${{ first: 3 }} | ${3} | ${4} | ${6} | ${true} | ${true} | ${SortOrder.Desc} | ||
| ${{ last: 5 }} | ${9} | ${4} | ${8} | ${true} | ${true} | ${SortOrder.Desc} | ||
| ${null} | ${null} | ${0} | ${19} | ${true} | ${false} | ${SortOrder.Asc} | ||
| ${{ first: 5 }} | ${null} | ${0} | ${4} | ${true} | ${false} | ${SortOrder.Asc} | ||
| ${{ first: 22 }} | ${null} | ${0} | ${21} | ${false} | ${false} | ${SortOrder.Asc} | ||
| ${{ first: 3 }} | ${3} | ${4} | ${6} | ${true} | ${true} | ${SortOrder.Asc} | ||
| ${{ last: 5 }} | ${9} | ${4} | ${8} | ${true} | ${true} | ${SortOrder.Asc} | ||
| `( | ||
| 'pagination $pagination with cursor $cursor in $sortOrder order', | ||
| async ({ | ||
| pagination, | ||
| cursor, | ||
| start, | ||
| end, | ||
| hasNextPage, | ||
| hasPreviousPage, | ||
| sortOrder | ||
| }): Promise<void> => { | ||
| if (sortOrder === SortOrder.Asc) { | ||
| modelsCreated.reverse() | ||
| } | ||
| if (cursor) { | ||
| if (pagination.last) pagination.before = modelsCreated[cursor].id | ||
| else pagination.after = modelsCreated[cursor].id | ||
| } | ||
|
|
||
| const page = await getPage(pagination, sortOrder) | ||
| const pageInfo = await getPageInfo({ | ||
| getPage: (pagination, sortOrder) => getPage(pagination, sortOrder), | ||
| page, | ||
| sortOrder | ||
| }) | ||
| expect(pageInfo).toEqual({ | ||
| startCursor: modelsCreated[start].id, | ||
| endCursor: modelsCreated[end].id, | ||
| hasNextPage, | ||
| hasPreviousPage | ||
| }) | ||
| } | ||
| ) | ||
| }) | ||
| } | ||
|
|
||
| test.todo('test suite must contain at least one test') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { | ||
| Model, | ||
| ModelOptions, | ||
| Page, | ||
| Pojo, | ||
| QueryBuilder, | ||
| QueryContext | ||
| } from 'objection' | ||
| import { DbErrors } from 'objection-db-errors' | ||
| import { v4 as uuid } from 'uuid' | ||
|
|
||
| export interface Pagination { | ||
| after?: string // Forward pagination: cursor. | ||
| before?: string // Backward pagination: cursor. | ||
| first?: number // Forward pagination: limit. | ||
| last?: number // Backward pagination: limit. | ||
| } | ||
|
|
||
| export interface PageInfo { | ||
| startCursor?: string | ||
| endCursor?: string | ||
| hasNextPage: boolean | ||
| hasPreviousPage: boolean | ||
| } | ||
|
|
||
| export enum SortOrder { | ||
| Asc = 'ASC', | ||
| Desc = 'DESC' | ||
| } | ||
|
|
||
| class PaginationQueryBuilder<M extends Model, R = M[]> extends QueryBuilder< | ||
| M, | ||
| R | ||
| > { | ||
| ArrayQueryBuilderType!: PaginationQueryBuilder<M, M[]> | ||
| SingleQueryBuilderType!: PaginationQueryBuilder<M, M> | ||
| MaybeSingleQueryBuilderType!: PaginationQueryBuilder<M, M | undefined> | ||
| NumberQueryBuilderType!: PaginationQueryBuilder<M, number> | ||
| PageQueryBuilderType!: PaginationQueryBuilder<M, Page<M>> | ||
|
|
||
| /** TODO: Base64 encode/decode the cursors | ||
| * Buffer.from("Hello World").toString('base64') | ||
| * Buffer.from("SGVsbG8gV29ybGQ=", 'base64').toString('ascii') | ||
| */ | ||
|
|
||
| /** getPage | ||
| * The pagination algorithm is based on the Relay connection specification. | ||
| * Please read the spec before changing things: | ||
| * https://relay.dev/graphql/connections.htm | ||
| * @param pagination Pagination - cursors and limits. | ||
| * @param sortOrder SortOrder - Asc/Desc sort order. | ||
| * @returns Model[] An array of Models that form a page. | ||
| */ | ||
| getPage( | ||
| pagination?: Pagination, | ||
| sortOrder: SortOrder = SortOrder.Desc | ||
| ): this { | ||
| const tableName = this.modelClass().tableName | ||
| if ( | ||
| typeof pagination?.before === 'undefined' && | ||
| typeof pagination?.last === 'number' | ||
| ) | ||
| throw new Error("Can't paginate backwards from the start.") | ||
|
|
||
| const first = pagination?.first || 20 | ||
| if (first < 0 || first > 100) throw new Error('Pagination index error') | ||
| const last = pagination?.last || 20 | ||
| if (last < 0 || last > 100) throw new Error('Pagination index error') | ||
| /** | ||
| * Forward pagination | ||
| */ | ||
| if (typeof pagination?.after === 'string') { | ||
| const comparisonOperator = sortOrder === SortOrder.Asc ? '>' : '<' | ||
| return this.whereRaw( | ||
| `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, | ||
| [this.modelClass().tableName, pagination.after] | ||
| ) | ||
| .orderBy([ | ||
| { column: 'createdAt', order: sortOrder }, | ||
| { column: 'id', order: sortOrder } | ||
| ]) | ||
| .limit(first) | ||
| } | ||
| /** | ||
| * Backward pagination | ||
| */ | ||
| if (typeof pagination?.before === 'string') { | ||
| const comparisonOperator = sortOrder === SortOrder.Asc ? '<' : '>' | ||
| const order = sortOrder === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc | ||
| return this.whereRaw( | ||
| `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, | ||
| [this.modelClass().tableName, pagination.before] | ||
| ) | ||
| .orderBy([ | ||
| { column: 'createdAt', order }, | ||
| { column: 'id', order } | ||
| ]) | ||
| .limit(last) | ||
| .runAfter((models) => { | ||
| if (Array.isArray(models)) { | ||
| return models.reverse() | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| return this.orderBy([ | ||
| { column: 'createdAt', order: sortOrder }, | ||
| { column: 'id', order: sortOrder } | ||
| ]).limit(first) | ||
| } | ||
| } | ||
|
|
||
| export class PaginationModel extends DbErrors(Model) { | ||
| QueryBuilderType!: PaginationQueryBuilder<this> | ||
| static QueryBuilder = PaginationQueryBuilder | ||
| } | ||
|
|
||
| export abstract class BaseModel extends PaginationModel { | ||
| public static get modelPaths(): string[] { | ||
| return [__dirname] | ||
| } | ||
|
|
||
| public id!: string | ||
| public createdAt!: Date | ||
| public updatedAt!: Date | ||
|
|
||
| public $beforeInsert(context: QueryContext): void { | ||
| super.$beforeInsert(context) | ||
| this.id = this.id || uuid() | ||
| } | ||
|
|
||
| public $beforeUpdate(_opts: ModelOptions, _queryContext: QueryContext): void { | ||
| this.updatedAt = new Date() | ||
| } | ||
|
|
||
| $formatJson(json: Pojo): Pojo { | ||
| json = super.$formatJson(json) | ||
| return { | ||
| ...json, | ||
| createdAt: json.createdAt.toISOString(), | ||
| updatedAt: json.updatedAt.toISOString() | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { TransactionOrKnex } from 'objection' | ||
|
|
||
| import { Logger } from 'pino' | ||
|
|
||
| export interface BaseService { | ||
| logger: Logger | ||
| knex?: TransactionOrKnex | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export interface FilterString { | ||
| in?: string[] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.