import path from "path" ; import bcrypt from "bcryptjs" ; import fs from "fs" ; import { v4 } from "uuid" ; import { uniqBy , map , sample , reject , includes , orderBy , flow , flatMap , curry , get , constant , filter , inRange , remove , } from "lodash/fp" ; import { isWithinInterval } from "date-fns" ; import low from "lowdb" ; import FileSync from "lowdb/adapters/FileSync" ; import shortid from "shortid" ; import { BankAccount , Transaction , User , Contact , TransactionStatus , TransactionRequestStatus , Like , Comment , PaymentNotification , PaymentNotificationStatus , LikeNotification , CommentNotification , NotificationType , NotificationPayloadType , NotificationsType , TransactionResponseItem , TransactionPayload , BankTransfer , BankTransferPayload , BankTransferType , NotificationResponseItem , TransactionQueryPayload , DefaultPrivacyLevel , } from "../src/models" ; import Fuse from "fuse.js" ; import { isPayment , getTransferAmount , hasSufficientFunds , getChargeAmount , hasDateQueryFields , getDateQueryFields , hasAmountQueryFields , getAmountQueryFields , getQueryWithoutFilterFields , getPayAppCreditedAmount , isRequestTransaction , formatFullName , isLikeNotification , isCommentNotification , } from "../src/utils/transactionUtils" ; import { DbSchema } from "../src/models/db-schema" ; export type TDatabase = { users : User [ ] ; contacts : Contact [ ] ; bankaccounts : BankAccount [ ] ; transactions : Transaction [ ] ; likes : Like [ ] ; comments : Comment [ ] ; notifications : NotificationType [ ] ; banktransfers : BankTransfer [ ] ; } ; const USER_TABLE = "users" ; const CONTACT_TABLE = "contacts" ; const BANK_ACCOUNT_TABLE = "bankaccounts" ; const TRANSACTION_TABLE = "transactions" ; const LIKE_TABLE = "likes" ; const COMMENT_TABLE = "comments" ; const NOTIFICATION_TABLE = "notifications" ; const BANK_TRANSFER_TABLE = "banktransfers" ; const databaseFile = path . join ( __dirname , "../data/database.json" ) ; const adapter = new FileSync < DbSchema > ( databaseFile ) ; const db = low ( adapter ) ; export const seedDatabase = ( ) => { const testSeed = JSON . parse ( fs . readFileSync ( path . join ( process . cwd ( ) , "data" , "database-seed.json" ) , "utf-8" ) ) ; db . setState ( testSeed ) . write ( ) ; return ; } ; export const getAllUsers = ( ) => db . get ( USER_TABLE ) . value ( ) ; export const getAllPublicTransactions = ( ) => db . get ( TRANSACTION_TABLE ) . filter ( { privacyLevel : DefaultPrivacyLevel . public } ) . value ( ) ; export const getAllForEntity = ( entity ) => db . get ( entity ) . value ( ) ; export const getAllBy = ( entity , key , value ) => { const result = db . get ( entity ) . filter ( { [ `${ key }` ] : value } ) . value ( ) ; return result ; } ; export const getBy = ( entity , key , value ) => { const result = db . get ( entity ) . find ( { [ `${ key }` ] : value } ) . value ( ) ; return result ; } ; export const getAllByObj = ( entity , query ) => { const result = db . get ( entity ) . filter ( query ) . value ( ) ; return result ; } ; export const cleanSearchQuery = ( query ) => query . replace ( /[^a-zA-Z0-9]/g , "" ) ; export const setupSearch = curry ( ( items , options , query ) => { const fuse = new Fuse ( items , options ) ; return fuse . search ( query ) ; } ) ; export const performSearch = ( items , options , query ) => flow ( cleanSearchQuery , setupSearch ( items , options ) , map ( ( result ) => result . item ) ) ( query ) ; export const searchUsers = ( query ) => { const items = getAllUsers ( ) ; return performSearch ( items , { keys : [ "firstName" , "lastName" , "username" , "email" , "phoneNumber" ] , } , query ) as User [ ] ; } ; export const removeUserFromResults = ( userId , results ) => remove ( { id : userId } , results ) ; export const getUserBy = ( key , value ) => getBy ( USER_TABLE , key , value ) ; export const getUserId = ( user ) : string => user . id ; export const getUserById = ( id ) => getUserBy ( "id" , id ) ; export const getUserByUsername = ( username ) => getUserBy ( "username" , username ) ; export const createUser = ( userDetails ) : User => { const password = bcrypt . hashSync ( userDetails . password ! , 10 ) ; const user = { id : shortid ( ) , uuid : v4 ( ) , firstName : userDetails . firstName ! , lastName : userDetails . lastName ! , username : userDetails . username ! , password , email : userDetails . email ! , phoneNumber : userDetails . phoneNumber ! , balance : userDetails . balance ! || 0 , avatar : userDetails . avatar ! , defaultPrivacyLevel : userDetails . defaultPrivacyLevel ! , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; saveUser ( user ) ; return user ; } ; const saveUser = ( user ) => { db . get ( USER_TABLE ) . push ( user ) . write ( ) ; } ; export const updateUserById = ( userId , edits ) => { const user = getUserById ( userId ) ; db . get ( USER_TABLE ) . find ( user ) . assign ( edits ) . write ( ) ; } ; export const getContactBy = ( key , value ) => getBy ( CONTACT_TABLE , key , value ) ; export const getContactsBy = ( key , value ) => getAllBy ( CONTACT_TABLE , key , value ) ; export const getContactsByUsername = ( username ) => flow ( getUserByUsername , getUserId , getContactsByUserId ) ( username ) ; export const getContactsByUserId = ( userId ) : Contact [ ] => getContactsBy ( "userId" , userId ) ; export const createContact = ( contact ) => { db . get ( CONTACT_TABLE ) . push ( contact ) . write ( ) ; return getContactBy ( "id" , contact . id ) ; } ; export const removeContactById = ( contactId ) => { const contact = getContactBy ( "id" , contactId ) ; db . get ( CONTACT_TABLE ) . remove ( contact ) . write ( ) ; } ; export const createContactForUser = ( userId , contactUserId ) => { const contactId = shortid ( ) ; const contact = { id : contactId , uuid : v4 ( ) , userId , contactUserId , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; const result = createContact ( contact ) ; return result ; } ; export const getBankAccountBy = ( key , value ) => getBy ( BANK_ACCOUNT_TABLE , key , value ) ; export const getBankAccountById = ( id ) => getBankAccountBy ( "id" , id ) ; export const getBankAccountsBy = ( key , value ) => getAllBy ( BANK_ACCOUNT_TABLE , key , value ) ; export const createBankAccount = ( bankaccount ) => { db . get ( BANK_ACCOUNT_TABLE ) . push ( bankaccount ) . write ( ) ; return getBankAccountBy ( "id" , bankaccount . id ) ; } ; export const createBankAccountForUser = ( userId , accountDetails ) => { const accountId = shortid ( ) ; const bankaccount = { id : accountId , uuid : v4 ( ) , userId , bankName : accountDetails . bankName ! , accountNumber : accountDetails . accountNumber ! , routingNumber : accountDetails . routingNumber ! , isDeleted : false , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; const result = createBankAccount ( bankaccount ) ; return result ; } ; export const removeBankAccountById = ( bankAccountId ) => { db . get ( BANK_ACCOUNT_TABLE ) . find ( { id : bankAccountId } ) . assign ( { isDeleted : true } ) . write ( ) ; } ; export const getBankTransferBy = ( key , value ) => getBy ( BANK_TRANSFER_TABLE , key , value ) ; export const getBankTransfersBy = ( key , value ) => getAllBy ( BANK_TRANSFER_TABLE , key , value ) ; export const getBankTransfersByUserId = ( userId ) => getBankTransfersBy ( "userId" , userId ) ; export const createBankTransfer = ( bankTransferDetails ) => { const bankTransfer = { id : shortid ( ) , uuid : v4 ( ) , ... bankTransferDetails , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; const savedBankTransfer = saveBankTransfer ( bankTransfer ) ; return savedBankTransfer ; } ; const saveBankTransfer = ( bankTransfer ) : BankTransfer => { db . get ( BANK_TRANSFER_TABLE ) . push ( bankTransfer ) . write ( ) ; return getBankTransferBy ( "id" , bankTransfer . id ) ; } ; export const getTransactionBy = ( key , value ) => getBy ( TRANSACTION_TABLE , key , value ) ; export const getTransactionById = ( id ) => getTransactionBy ( "id" , id ) ; export const getTransactionsByObj = ( query ) => getAllByObj ( TRANSACTION_TABLE , query ) ; export const getTransactionByIdForApi = ( id ) => formatTransactionForApiResponse ( getTransactionBy ( "id" , id ) ) ; export const getTransactionsForUserForApi = ( userId , query ? ) => flow ( getTransactionsForUserByObj ( userId ) , formatTransactionsForApiResponse ) ( query ) ; export const getFullNameForUser = ( userId ) => flow ( getUserById , formatFullName ) ( userId ) ; export const formatTransactionForApiResponse = ( transaction ) : TransactionResponseItem => { const receiver = getUserById ( transaction . receiverId ) ; const sender = getUserById ( transaction . senderId ) ; const receiverName = getFullNameForUser ( transaction . receiverId ) ; const senderName = getFullNameForUser ( transaction . senderId ) ; const likes = getLikesByTransactionId ( transaction . id ) ; const comments = getCommentsByTransactionId ( transaction . id ) ; return { receiverName , senderName , receiverAvatar : receiver . avatar , senderAvatar : sender . avatar , likes , comments , ... transaction , } ; } ; export const formatTransactionsForApiResponse = ( transactions ) : TransactionResponseItem [ ] => orderBy ( [ ( transaction ) => new Date ( transaction . modifiedAt ) ] , [ "desc" ] , transactions . map ( ( transaction ) => formatTransactionForApiResponse ( transaction ) ) ) ; export const getAllTransactionsForUserByObj = curry ( ( userId , query ? ) => { const queryWithoutFilterFields = query && getQueryWithoutFilterFields ( query ) ; const queryFields = queryWithoutFilterFields || query ; const userTransactions = flatMap ( getTransactionsByObj ) ( [ { receiverId : userId , ... queryFields , } , { senderId : userId , ... queryFields , } , ] ) ; if ( query && ( hasDateQueryFields ( query ) || hasAmountQueryFields ( query ) ) ) { const { dateRangeStart , dateRangeEnd } = getDateQueryFields ( query ) ; const { amountMin , amountMax } = getAmountQueryFields ( query ) ; return flow ( transactionsWithinDateRange ( dateRangeStart ! , dateRangeEnd ! ) , transactionsWithinAmountRange ( amountMin ! , amountMax ! ) ) ( userTransactions ) ; } return userTransactions ; } ) ; export const transactionsWithinAmountRange = curry ( ( amountMin , amountMax , transactions ) => { if ( ! amountMin || ! amountMax ) { return transactions ; } return filter ( ( transaction ) => inRange ( amountMin , amountMax , transaction . amount ) , transactions ) ; } ) ; export const transactionsWithinDateRange = curry ( ( dateRangeStart , dateRangeEnd , transactions ) => { if ( ! dateRangeStart || ! dateRangeEnd ) { return transactions ; } return filter ( ( transaction ) => isWithinInterval ( new Date ( transaction . createdAt ) , { start : new Date ( dateRangeStart ) , end : new Date ( dateRangeEnd ) , } ) , transactions ) ; } ) ; export const getTransactionsForUserByObj = curry ( ( userId , query ) => flow ( getAllTransactionsForUserByObj ( userId ) , uniqBy ( "id" ) ) ( query ) ) ; export const getContactIdsForUser = ( userId ) : Contact [ "id" ] [ ] => flow ( getContactsByUserId , map ( "contactUserId" ) ) ( userId ) ; export const getTransactionsForUserContacts = ( userId , query ? ) => uniqBy ( "id" , flatMap ( ( contactId ) => getTransactionsForUserForApi ( contactId , query ) , getContactIdsForUser ( userId ) ) ) ; export const getTransactionIds = ( transactions ) => map ( "id" , transactions ) ; export const getContactsTransactionIds = ( userId ) : Transaction [ "id" ] [ ] => flow ( getTransactionsForUserContacts , getTransactionIds ) ( userId ) ; export const nonContactPublicTransactions = ( userId ) : Transaction [ ] => { const contactsTransactionIds = getContactsTransactionIds ( userId ) ; return flow ( getAllPublicTransactions , reject ( ( transaction ) => includes ( transaction . id , contactsTransactionIds ) ) ) ( ) ; } ; export const getNonContactPublicTransactionsForApi = ( userId ) => flow ( nonContactPublicTransactions , formatTransactionsForApiResponse ) ( userId ) ; export const getPublicTransactionsDefaultSort = ( userId ) => ( { contactsTransactions : getTransactionsForUserContacts ( userId ) , publicTransactions : getNonContactPublicTransactionsForApi ( userId ) , } ) ; export const getPublicTransactionsByQuery = ( userId , query ) => { if ( query && ( hasDateQueryFields ( query ) || hasAmountQueryFields ( query ) ) ) { const { dateRangeStart , dateRangeEnd } = getDateQueryFields ( query ) ; const { amountMin , amountMax } = getAmountQueryFields ( query ) ; return { contactsTransactions : getTransactionsForUserContacts ( userId , query ) , publicTransactions : flow ( transactionsWithinDateRange ( dateRangeStart ! , dateRangeEnd ! ) , transactionsWithinAmountRange ( amountMin ! , amountMax ! ) ) ( getNonContactPublicTransactionsForApi ( userId ) ) , } ; } else { return { contactsTransactions : getTransactionsForUserContacts ( userId ) , publicTransactions : getNonContactPublicTransactionsForApi ( userId ) , } ; } } ; export const resetPayAppBalance = constant ( 0 ) ; export const debitPayAppBalance = ( user , transaction ) => { if ( hasSufficientFunds ( user , transaction ) ) { flow ( getChargeAmount , savePayAppBalance ( user ) ) ( user , transaction ) ; } else { flow ( getTransferAmount ( user ) , createBankTransferWithdrawal ( user , transaction ) , resetPayAppBalance , savePayAppBalance ( user ) ) ( transaction ) ; } } ; export const creditPayAppBalance = ( user , transaction ) => flow ( getPayAppCreditedAmount , savePayAppBalance ( user ) ) ( user , transaction ) ; export const createBankTransferWithdrawal = curry ( ( sender , transaction , transferAmount ) => createBankTransfer ( { userId : sender . id , source : transaction . source , amount : transferAmount , transactionId : transaction . id , type : BankTransferType . withdrawal , } ) ) ; export const savePayAppBalance = curry ( ( sender , balance ) => updateUserById ( get ( "id" , sender ) , { balance } ) ) ; export const createTransaction = ( userId , transactionType , transactionDetails ) : Transaction => { const sender = getUserById ( userId ) ; const receiver = getUserById ( transactionDetails . receiverId ) ; const transaction = { id : shortid ( ) , uuid : v4 ( ) , source : transactionDetails . source , amount : transactionDetails . amount * 100 , description : transactionDetails . description , receiverId : transactionDetails . receiverId , senderId : userId , privacyLevel : transactionDetails . privacyLevel || sender . defaultPrivacyLevel , status : TransactionStatus . pending , requestStatus : transactionType === "request" ? TransactionRequestStatus . pending : undefined , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; const savedTransaction = saveTransaction ( transaction ) ; if ( isPayment ( transaction ) ) { debitPayAppBalance ( sender , transaction ) ; creditPayAppBalance ( receiver , transaction ) ; updateTransactionById ( transaction . id , { status : TransactionStatus . complete , } ) ; createPaymentNotification ( transaction . receiverId , transaction . id , PaymentNotificationStatus . received ) ; } else { createPaymentNotification ( transaction . receiverId , transaction . id , PaymentNotificationStatus . requested ) ; } return savedTransaction ; } ; const saveTransaction = ( transaction ) : Transaction => { db . get ( TRANSACTION_TABLE ) . push ( transaction ) . write ( ) ; return getTransactionBy ( "id" , transaction . id ) ; } ; export const updateTransactionById = ( transactionId , edits ) => { const transaction = getTransactionBy ( "id" , transactionId ) ; const { senderId , receiverId } = transaction ; const sender = getUserById ( senderId ) ; const receiver = getUserById ( receiverId ) ; if ( isRequestTransaction ( transaction ) ) { debitPayAppBalance ( receiver , transaction ) ; creditPayAppBalance ( sender , transaction ) ; edits . status = TransactionStatus . complete ; createPaymentNotification ( transaction . senderId , transaction . id , PaymentNotificationStatus . received ) ; } db . get ( TRANSACTION_TABLE ) . find ( transaction ) . assign ( edits ) . write ( ) ; } ; export const getLikeBy = ( key , value ) : Like => getBy ( LIKE_TABLE , key , value ) ; export const getLikesByObj = ( query ) => getAllByObj ( LIKE_TABLE , query ) ; export const getLikeById = ( id ) : Like => getLikeBy ( "id" , id ) ; export const getLikesByTransactionId = ( transactionId ) => getLikesByObj ( { transactionId } ) ; export const createLike = ( userId , transactionId ) : Like => { const like = { id : shortid ( ) , uuid : v4 ( ) , userId , transactionId , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; const savedLike = saveLike ( like ) ; return savedLike ; } ; export const createLikes = ( userId , transactionId ) => { const { senderId , receiverId } = getTransactionById ( transactionId ) ; const like = createLike ( userId , transactionId ) ; if ( userId !== senderId || userId !== receiverId ) { createLikeNotification ( senderId , transactionId , like . id ) ; createLikeNotification ( receiverId , transactionId , like . id ) ; } else if ( userId === senderId ) { createLikeNotification ( senderId , transactionId , like . id ) ; } else { createLikeNotification ( receiverId , transactionId , like . id ) ; } } ; const saveLike = ( like ) : Like => { db . get ( LIKE_TABLE ) . push ( like ) . write ( ) ; return getLikeById ( like . id ) ; } ; export const getCommentBy = ( key , value ) : Comment => getBy ( COMMENT_TABLE , key , value ) ; export const getCommentsByObj = ( query ) => getAllByObj ( COMMENT_TABLE , query ) ; export const getCommentById = ( id ) : Comment => getCommentBy ( "id" , id ) ; export const getCommentsByTransactionId = ( transactionId ) => getCommentsByObj ( { transactionId } ) ; export const createComment = ( userId , transactionId , content ) : Comment => { const comment = { id : shortid ( ) , uuid : v4 ( ) , content , userId , transactionId , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; const savedComment = saveComment ( comment ) ; return savedComment ; } ; export const createComments = ( userId , transactionId , content ) => { const { senderId , receiverId } = getTransactionById ( transactionId ) ; const comment = createComment ( userId , transactionId , content ) ; if ( userId !== senderId || userId !== receiverId ) { createCommentNotification ( senderId , transactionId , comment . id ) ; createCommentNotification ( receiverId , transactionId , comment . id ) ; } else if ( userId === senderId ) { createCommentNotification ( senderId , transactionId , comment . id ) ; } else { createCommentNotification ( receiverId , transactionId , comment . id ) ; } } ; const saveComment = ( comment ) : Comment => { db . get ( COMMENT_TABLE ) . push ( comment ) . write ( ) ; return getCommentById ( comment . id ) ; } ; export const getNotificationBy = ( key , value ) : NotificationType => getBy ( NOTIFICATION_TABLE , key , value ) ; export const getNotificationsByObj = ( query ) : Notification [ ] => getAllByObj ( NOTIFICATION_TABLE , query ) ; export const getUnreadNotificationsByUserId = ( userId ) => flow ( getNotificationsByObj , formatNotificationsForApiResponse ) ( { userId , isRead : false } ) ; export const createPaymentNotification = ( userId , transactionId , status ) : PaymentNotification => { const notification = { id : shortid ( ) , uuid : v4 ( ) , userId : userId , transactionId : transactionId , status , isRead : false , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; saveNotification ( notification ) ; return notification ; } ; export const createLikeNotification = ( userId , transactionId , likeId ) : LikeNotification => { const notification = { id : shortid ( ) , uuid : v4 ( ) , userId : userId , transactionId : transactionId , likeId : likeId , isRead : false , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; saveNotification ( notification ) ; return notification ; } ; export const createCommentNotification = ( userId , transactionId , commentId ) : CommentNotification => { const notification = { id : shortid ( ) , uuid : v4 ( ) , userId : userId , transactionId : transactionId , commentId : commentId , isRead : false , createdAt : new Date ( ) , modifiedAt : new Date ( ) , } ; saveNotification ( notification ) ; return notification ; } ; const saveNotification = ( notification ) => { db . get ( NOTIFICATION_TABLE ) . push ( notification ) . write ( ) ; } ; export const createNotifications = ( userId , notifications ) => notifications . flatMap ( ( item ) => { if ( "status" in item && item . type === NotificationsType . payment ) { return createPaymentNotification ( userId , item . transactionId , item . status ) ; } else if ( "likeId" in item && item . type === NotificationsType . like ) { return createLikeNotification ( userId , item . transactionId , item . likeId ) ; } else { if ( "commentId" in item ) { return createCommentNotification ( userId , item . transactionId , item . commentId ) ; } } } ) ; export const updateNotificationById = ( userId , notificationId , edits ) => { const notification = getNotificationBy ( "id" , notificationId ) ; db . get ( NOTIFICATION_TABLE ) . find ( notification ) . assign ( edits ) . write ( ) ; } ; export const formatNotificationForApiResponse = ( notification ) : NotificationResponseItem => { let userFullName = getFullNameForUser ( notification . userId ) ; const transaction = getTransactionById ( notification . transactionId ) ; if ( isRequestTransaction ( transaction ) ) { userFullName = getFullNameForUser ( transaction . senderId ) ; } if ( isLikeNotification ( notification ) ) { const like = getLikeById ( notification . likeId ) ; userFullName = getFullNameForUser ( like . userId ) ; } if ( isCommentNotification ( notification ) ) { const comment = getCommentById ( notification . commentId ) ; userFullName = getFullNameForUser ( comment . userId ) ; } return { userFullName , ... notification , } ; } ; export const formatNotificationsForApiResponse = ( notifications ) : NotificationResponseItem [ ] => orderBy ( [ ( notification ) => new Date ( notification . modifiedAt ) ] , [ "desc" ] , notifications . map ( ( notification ) => formatNotificationForApiResponse ( notification ) ) ) ; export const getRandomUser = ( ) => { const users = getAllUsers ( ) ; return sample ( users ) ! ; } ; export const getAllContacts = ( ) => db . get ( CONTACT_TABLE ) . value ( ) ; export const getAllTransactions = ( ) => db . get ( TRANSACTION_TABLE ) . value ( ) ; export const getBankAccountsByUserId = ( userId ) => getBankAccountsBy ( "userId" , userId ) ; export const getNotificationById = ( id ) : NotificationType => getNotificationBy ( "id" , id ) ; export const getNotificationsByUserId = ( userId ) => getNotificationsByObj ( { userId } ) ; export const getBankTransferByTransactionId = ( transactionId ) => getBankTransferBy ( "transactionId" , transactionId ) ; export const getTransactionsBy = ( key , value ) => getAllBy ( TRANSACTION_TABLE , key , value ) ; export const getTransactionsByUserId = ( userId ) => getTransactionsBy ( "receiverId" , userId ) ; export default db ;