diff --git a/bats/admin-gql/merchant-map-validate.gql b/bats/admin-gql/merchant-map-validate.gql new file mode 100644 index 00000000000..008819ae370 --- /dev/null +++ b/bats/admin-gql/merchant-map-validate.gql @@ -0,0 +1,11 @@ +mutation merchantMapValidate($input: MerchantMapValidateInput!) { + merchantMapValidate(input: $input) { + errors { + message + } + merchant { + id + validated + } + } +} \ No newline at end of file diff --git a/bats/admin-gql/merchants-pending-approval.gql b/bats/admin-gql/merchants-pending-approval.gql new file mode 100644 index 00000000000..80aeb3d30f2 --- /dev/null +++ b/bats/admin-gql/merchants-pending-approval.gql @@ -0,0 +1,5 @@ +query merchantsPendingApproval { + merchantsPendingApproval { + id + } +} diff --git a/bats/core/api/merchant.bats b/bats/core/api/merchant.bats index 89889f04a59..f7b77e03b83 100644 --- a/bats/core/api/merchant.bats +++ b/bats/core/api/merchant.bats @@ -13,8 +13,9 @@ setup_file() { login_admin } -@test "merchant: add a merchant with admin api" { - admin_token="$(read_value 'admin.token')" +@test "merchant: suggest a merchant" { + token_name='merchant' + latitude=40.712776 longitude=-74.005974 title="My Merchant" @@ -28,12 +29,36 @@ setup_file() { '{input: {latitude: ($latitude | tonumber), longitude: ($longitude | tonumber), title: $title, username: $username}}' ) - exec_admin_graphql $admin_token 'business-update-map-info' "$variables" - latitude_result="$(graphql_output '.data.businessUpdateMapInfo.merchant.coordinates.latitude')" + exec_graphql "$token_name" 'merchant-map-suggest' "$variables" + latitude_result="$(graphql_output '.data.merchantMapSuggest.merchant.coordinates.latitude')" [[ "$latitude_result" == "$latitude" ]] || exit 1 + + # no merchant visible yet + exec_graphql 'anon' 'business-map-markers' + map_markers="$(graphql_output)" + markers_length=$(echo "$map_markers" | jq '.data.businessMapMarkers | length') + + [[ $markers_length -eq 0 ]] || exit 1 } -@test "merchant: can query merchants" { +@test "merchant: listing and approving merchant waiting for approval" { + admin_token="$(read_value 'admin.token')" + local username="$(read_value merchant.username)" + + exec_admin_graphql $admin_token 'merchants-pending-approval' + id="$(graphql_output '.data.merchantsPendingApproval[0].id')" + [[ "$id" != "null" && "$id" != "" ]] || exit 1 + + # validating merchant + variables=$(jq -n \ + --arg id "$id" \ + '{input: {id: $id}}' + ) + exec_admin_graphql $admin_token 'merchant-map-validate' "$variables" + validate_status="$(graphql_output '.data.merchantMapValidate.merchant.validated')" + [[ "$validate_status" == "true" ]] || exit 1 + + # merchant is now visible from public api local username="$(read_value merchant.username)" exec_graphql 'anon' 'business-map-markers' fetch_username="$(graphql_output '.data.businessMapMarkers[0].username')" @@ -41,6 +66,8 @@ setup_file() { } @test "merchant: delete merchant with admin api" { + "skip" + admin_token="$(read_value 'admin.token')" local username="$(read_value merchant.username)" diff --git a/bats/admin-gql/business-update-map-info.gql b/bats/gql/merchant-map-suggest.gql similarity index 57% rename from bats/admin-gql/business-update-map-info.gql rename to bats/gql/merchant-map-suggest.gql index 8920ddb7d40..799b506743c 100644 --- a/bats/admin-gql/business-update-map-info.gql +++ b/bats/gql/merchant-map-suggest.gql @@ -1,18 +1,17 @@ -mutation businessUpdateMapInfo($input: BusinessUpdateMapInfoInput!) { - businessUpdateMapInfo(input: $input) { +mutation merchantMapSuggest($input: MerchantMapSuggestInput!) { + merchantMapSuggest(input: $input) { errors { message } merchant { id + validated title coordinates { latitude longitude } username - validated - createdAt } } } diff --git a/core/api/src/app/merchants/approve-merchant-map.ts b/core/api/src/app/merchants/approve-merchant-map.ts new file mode 100644 index 00000000000..08928c51256 --- /dev/null +++ b/core/api/src/app/merchants/approve-merchant-map.ts @@ -0,0 +1,13 @@ +import { MerchantsRepository } from "@/services/mongoose" + +export const approveMerchantById = async ( + id: MerchantId, +): Promise => { + const merchantsRepo = MerchantsRepository() + + const merchant = await merchantsRepo.findById(id) + if (merchant instanceof Error) return merchant + + merchant.validated = true + return merchantsRepo.update(merchant) +} diff --git a/core/api/src/app/merchants/delete-business-map.ts b/core/api/src/app/merchants/delete-merchant-map.ts similarity index 100% rename from core/api/src/app/merchants/delete-business-map.ts rename to core/api/src/app/merchants/delete-merchant-map.ts diff --git a/core/api/src/app/merchants/index.ts b/core/api/src/app/merchants/index.ts index eae7d3f4675..bca26701c53 100644 --- a/core/api/src/app/merchants/index.ts +++ b/core/api/src/app/merchants/index.ts @@ -1,10 +1,15 @@ import { MerchantsRepository } from "@/services/mongoose" -export * from "./update-business-map" -export * from "./delete-business-map" +export * from "./suggest-merchant-map" +export * from "./delete-merchant-map" +export * from "./approve-merchant-map" const merchants = MerchantsRepository() export const getMerchantsMapMarkers = async () => { return merchants.listForMap() } + +export const getMerchantsPendingApproval = async () => { + return merchants.listPendingApproval() +} diff --git a/core/api/src/app/merchants/suggest-merchant-map.ts b/core/api/src/app/merchants/suggest-merchant-map.ts new file mode 100644 index 00000000000..b3de91d6da5 --- /dev/null +++ b/core/api/src/app/merchants/suggest-merchant-map.ts @@ -0,0 +1,36 @@ +import { checkedCoordinates, checkedMapTitle, checkedToUsername } from "@/domain/accounts" +import { AccountsRepository, MerchantsRepository } from "@/services/mongoose" + +export const suggestMerchantMap = async ({ + username, + coordinates: { latitude, longitude }, + title, +}: { + username: string + coordinates: { latitude: number; longitude: number } + title: string +}): Promise => { + const merchantsRepo = MerchantsRepository() + + const usernameChecked = checkedToUsername(username) + if (usernameChecked instanceof Error) return usernameChecked + + const coordinates = checkedCoordinates({ latitude, longitude }) + if (coordinates instanceof Error) return coordinates + + const titleChecked = checkedMapTitle(title) + if (titleChecked instanceof Error) return titleChecked + + const accountRepository = AccountsRepository() + const account = await accountRepository.findByUsername(usernameChecked) + if (account instanceof Error) { + return account + } + + return merchantsRepo.create({ + username: usernameChecked, + coordinates, + title: titleChecked, + validated: false, + }) +} diff --git a/core/api/src/app/merchants/update-business-map.ts b/core/api/src/app/merchants/update-business-map.ts deleted file mode 100644 index fbd28650821..00000000000 --- a/core/api/src/app/merchants/update-business-map.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { checkedCoordinates, checkedMapTitle, checkedToUsername } from "@/domain/accounts" -import { CouldNotFindMerchantFromUsernameError } from "@/domain/errors" -import { MerchantsRepository } from "@/services/mongoose" - -export const updateBusinessMap = async ({ - username, - coordinates: { latitude, longitude }, - title, -}: { - username: string - coordinates: { latitude: number; longitude: number } - title: string -}): Promise => { - const merchantsRepo = MerchantsRepository() - - const usernameChecked = checkedToUsername(username) - if (usernameChecked instanceof Error) return usernameChecked - - const coordinates = checkedCoordinates({ latitude, longitude }) - if (coordinates instanceof Error) return coordinates - - const titleChecked = checkedMapTitle(title) - if (titleChecked instanceof Error) return titleChecked - - const merchants = await merchantsRepo.findByUsername(usernameChecked) - - if (merchants instanceof CouldNotFindMerchantFromUsernameError) { - return merchantsRepo.create({ - username: usernameChecked, - coordinates, - title: titleChecked, - validated: true, - }) - } else if (merchants instanceof Error) { - return merchants - } - - // TODO: manage multiple merchants for a single username - const merchant = merchants[0] - - merchant.coordinates = coordinates - merchant.title = titleChecked - - return merchantsRepo.update(merchant) -} diff --git a/core/api/src/graphql/admin/mutations.ts b/core/api/src/graphql/admin/mutations.ts index 794ff0aec1b..0890b413a41 100644 --- a/core/api/src/graphql/admin/mutations.ts +++ b/core/api/src/graphql/admin/mutations.ts @@ -1,11 +1,11 @@ import UserUpdatePhoneMutation from "./root/mutation/user-update-phone" -import BusinessDeleteMapInfoMutation from "./root/mutation/delete-business-map" +import MerchantMapDeleteMutation from "./root/mutation/merchant-map-delete" +import MerchantMapValidateMutation from "./root/mutation/merchant-map-validate" import AccountUpdateLevelMutation from "./root/mutation/account-update-level" import AccountUpdateStatusMutation from "./root/mutation/account-update-status" import AdminPushNotificationSendMutation from "./root/mutation/admin-push-notification-send" -import BusinessUpdateMapInfoMutation from "./root/mutation/business-update-map-info" import { GT } from "@/graphql/index" @@ -15,8 +15,8 @@ export const mutationFields = { userUpdatePhone: UserUpdatePhoneMutation, accountUpdateLevel: AccountUpdateLevelMutation, accountUpdateStatus: AccountUpdateStatusMutation, - businessUpdateMapInfo: BusinessUpdateMapInfoMutation, - businessDeleteMapInfo: BusinessDeleteMapInfoMutation, + merchantMapValidate: MerchantMapValidateMutation, + merchantMapDelete: MerchantMapDeleteMutation, adminPushNotificationSend: AdminPushNotificationSendMutation, }, } diff --git a/core/api/src/graphql/admin/queries.ts b/core/api/src/graphql/admin/queries.ts index 8f9e616c695..8c538bc97b7 100644 --- a/core/api/src/graphql/admin/queries.ts +++ b/core/api/src/graphql/admin/queries.ts @@ -10,6 +10,7 @@ import ListWalletIdsQuery from "./root/query/all-walletids" import WalletQuery from "./root/query/wallet" import AccountDetailsByAccountId from "./root/query/account-details-by-account-id" import AccountDetailsByUserId from "./root/query/account-details-by-user-id" +import MerchantsPendingApprovalQuery from "./root/query/merchants-pending-approval-listing" import { GT } from "@/graphql/index" @@ -28,6 +29,7 @@ export const queryFields = { lightningPayment: LightningPaymentQuery, listWalletIds: ListWalletIdsQuery, wallet: WalletQuery, + merchantsPendingApproval: MerchantsPendingApprovalQuery, }, } diff --git a/core/api/src/graphql/admin/root/mutation/delete-business-map.ts b/core/api/src/graphql/admin/root/mutation/merchant-map-delete.ts similarity index 74% rename from core/api/src/graphql/admin/root/mutation/delete-business-map.ts rename to core/api/src/graphql/admin/root/mutation/merchant-map-delete.ts index aff878b5c5a..122c23bf329 100644 --- a/core/api/src/graphql/admin/root/mutation/delete-business-map.ts +++ b/core/api/src/graphql/admin/root/mutation/merchant-map-delete.ts @@ -2,10 +2,11 @@ import { Merchants } from "@/app" import AccountDetailPayload from "@/graphql/admin/types/payload/account-detail" import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" import { GT } from "@/graphql/index" +import MerchantPayload from "@/graphql/shared/types/payload/merchant" import Username from "@/graphql/shared/types/scalar/username" -const BusinessDeleteMapInfoInput = GT.Input({ - name: "BusinessDeleteMapInfoInput", +const MerchantMapDeleteInput = GT.Input({ + name: "MerchantMapDeleteInput", fields: () => ({ username: { type: GT.NonNull(Username), @@ -13,7 +14,7 @@ const BusinessDeleteMapInfoInput = GT.Input({ }), }) -const BusinessDeleteMapInfoMutation = GT.Field< +const MerchantMapDeleteMutation = GT.Field< null, GraphQLAdminContext, { @@ -25,9 +26,9 @@ const BusinessDeleteMapInfoMutation = GT.Field< extensions: { complexity: 120, }, - type: GT.NonNull(AccountDetailPayload), + type: GT.NonNull(MerchantPayload), args: { - input: { type: GT.NonNull(BusinessDeleteMapInfoInput) }, + input: { type: GT.NonNull(MerchantMapDeleteInput) }, }, resolve: async (_, args) => { const { username } = args.input @@ -49,4 +50,4 @@ const BusinessDeleteMapInfoMutation = GT.Field< }, }) -export default BusinessDeleteMapInfoMutation +export default MerchantMapDeleteMutation diff --git a/core/api/src/graphql/admin/root/mutation/merchant-map-validate.ts b/core/api/src/graphql/admin/root/mutation/merchant-map-validate.ts new file mode 100644 index 00000000000..9245de48b6f --- /dev/null +++ b/core/api/src/graphql/admin/root/mutation/merchant-map-validate.ts @@ -0,0 +1,44 @@ +import { Merchants } from "@/app" +import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" +import { GT } from "@/graphql/index" +import MerchantPayload from "@/graphql/shared/types/payload/merchant" + +const MerchantMapValidateInput = GT.Input({ + name: "MerchantMapValidateInput", + fields: () => ({ + id: { + // TODO: MerchantID? + type: GT.NonNullID, + }, + }), +}) + +const MerchantMapValidate = GT.Field({ + extensions: { + complexity: 120, + }, + type: GT.NonNull(MerchantPayload), + args: { + input: { type: GT.NonNull(MerchantMapValidateInput) }, + }, + resolve: async (_, args) => { + const { id } = args.input + + if (id instanceof Error) { + return { errors: [{ message: id.message }] } + } + + const merchant = await Merchants.approveMerchantById(id) + + if (merchant instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(merchant)] } + } + + return { + errors: [], + merchant, + } + }, +}) + +export default MerchantMapValidate diff --git a/core/api/src/graphql/admin/root/query/merchants-pending-approval-listing.ts b/core/api/src/graphql/admin/root/query/merchants-pending-approval-listing.ts new file mode 100644 index 00000000000..9c9a2aa6891 --- /dev/null +++ b/core/api/src/graphql/admin/root/query/merchants-pending-approval-listing.ts @@ -0,0 +1,22 @@ +import { GT } from "@/graphql/index" + +import { Merchants } from "@/app" +import { mapError } from "@/graphql/error-map" +import Merchant from "@/graphql/shared/types/object/merchant" + +const MerchantsPendingApprovalQuery = GT.Field({ + type: GT.List(Merchant), + resolve: async (_, { id }) => { + if (id instanceof Error) throw id + + const merchants = await Merchants.getMerchantsPendingApproval() + + if (merchants instanceof Error) { + throw mapError(merchants) + } + + return merchants + }, +}) + +export default MerchantsPendingApprovalQuery diff --git a/core/api/src/graphql/admin/schema.graphql b/core/api/src/graphql/admin/schema.graphql index 98d92479657..2dbc23f469d 100644 --- a/core/api/src/graphql/admin/schema.graphql +++ b/core/api/src/graphql/admin/schema.graphql @@ -57,22 +57,6 @@ type AuditedAccount { wallets: [Wallet!]! } -type AuditedMerchant { - """ - GPS coordinates for the merchant that can be used to place the related business on a map - """ - coordinates: Coordinates - createdAt: Timestamp! - id: ID! - title: String! - - """The username of the merchant""" - username: Username! - - """Whether the merchant has been validated""" - validated: Boolean! -} - type AuditedUser { createdAt: Timestamp! @@ -152,17 +136,6 @@ type BTCWallet implements Wallet { walletCurrency: WalletCurrency! } -input BusinessDeleteMapInfoInput { - username: Username! -} - -input BusinessUpdateMapInfoInput { - latitude: Float! - longitude: Float! - title: String! - username: Username! -} - type Coordinates { latitude: Float! longitude: Float! @@ -294,17 +267,41 @@ scalar LnPubkey """Text field in a lightning payment transaction""" scalar Memo +type Merchant { + """ + GPS coordinates for the merchant that can be used to place the related business on a map + """ + coordinates: Coordinates + createdAt: Timestamp! + id: ID! + title: String! + + """The username of the merchant""" + username: Username! + + """Whether the merchant has been validated""" + validated: Boolean! +} + +input MerchantMapDeleteInput { + username: Username! +} + +input MerchantMapValidateInput { + username: String! +} + type MerchantPayload { errors: [Error!]! - merchant: AuditedMerchant + merchant: Merchant } type Mutation { accountUpdateLevel(input: AccountUpdateLevelInput!): AccountDetailPayload! accountUpdateStatus(input: AccountUpdateStatusInput!): AccountDetailPayload! adminPushNotificationSend(input: AdminPushNotificationSendInput!): AdminPushNotificationSendPayload! - businessDeleteMapInfo(input: BusinessDeleteMapInfoInput!): AccountDetailPayload! - businessUpdateMapInfo(input: BusinessUpdateMapInfoInput!): MerchantPayload! + merchantMapDelete(input: MerchantMapDeleteInput!): MerchantPayload! + merchantMapValidate(input: MerchantMapValidateInput!): MerchantPayload! userUpdatePhone(input: UserUpdatePhoneInput!): AccountDetailPayload! } @@ -363,6 +360,7 @@ type Query { lightningInvoice(hash: PaymentHash!): LightningInvoice! lightningPayment(hash: PaymentHash!): LightningPayment! listWalletIds(walletCurrency: WalletCurrency!): [WalletId!]! + merchantsPendingApproval: [Merchant] transactionById(id: ID!): Transaction transactionsByHash(hash: PaymentHash!): [Transaction] wallet(walletId: WalletId!): Wallet! diff --git a/core/api/src/graphql/admin/types/object/merchant.ts b/core/api/src/graphql/admin/types/object/merchant.ts deleted file mode 100644 index 9c2a97d63e6..00000000000 --- a/core/api/src/graphql/admin/types/object/merchant.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GraphQLObjectType } from "graphql" - -import { GT } from "@/graphql/index" -import Coordinates from "@/graphql/shared/types/object/coordinates" -import Timestamp from "@/graphql/shared/types/scalar/timestamp" -import Username from "@/graphql/shared/types/scalar/username" - -const AuditedMerchant: GraphQLObjectType = - GT.Object({ - name: "AuditedMerchant", - fields: () => ({ - id: { type: GT.NonNullID }, - title: { type: GT.NonNull(GT.String) }, - coordinates: { - type: Coordinates, - description: - "GPS coordinates for the merchant that can be used to place the related business on a map", - }, - validated: { - type: GT.NonNull(GT.Boolean), - description: "Whether the merchant has been validated", - }, - username: { - type: GT.NonNull(Username), - description: "The username of the merchant", - }, - createdAt: { - type: GT.NonNull(Timestamp), - resolve: (source) => source.createdAt, - }, - }), - }) - -export default AuditedMerchant diff --git a/core/api/src/graphql/public/mutations.ts b/core/api/src/graphql/public/mutations.ts index d5a4e756b60..82aff78f4f3 100644 --- a/core/api/src/graphql/public/mutations.ts +++ b/core/api/src/graphql/public/mutations.ts @@ -63,6 +63,7 @@ import UserUpdateUsernameMutation from "@/graphql/public/root/mutation/user-upda import CaptchaCreateChallengeMutation from "@/graphql/public/root/mutation/captcha-create-challenge" import CaptchaRequestAuthCodeMutation from "@/graphql/public/root/mutation/captcha-request-auth-code" import QuizClaimMutation from "@/graphql/public/root/mutation/quiz-claim" +import MerchantMapSuggest from "@/graphql/public/root/mutation/merchant-map-suggest" // TODO: // const fields: { [key: string]: GraphQLFieldConfig } export const mutationFields = { @@ -112,6 +113,9 @@ export const mutationFields = { callbackEndpointAdd: CallbackEndpointAdd, callbackEndpointDelete: CallbackEndpointDelete, + + // behind authed to prevent DDOS. not strictly necessary + merchantMapSuggest: MerchantMapSuggest, }, atWalletLevel: { diff --git a/core/api/src/graphql/admin/root/mutation/business-update-map-info.ts b/core/api/src/graphql/public/root/mutation/merchant-map-suggest.ts similarity index 74% rename from core/api/src/graphql/admin/root/mutation/business-update-map-info.ts rename to core/api/src/graphql/public/root/mutation/merchant-map-suggest.ts index 5a0ad4b5783..2b44aacffd4 100644 --- a/core/api/src/graphql/admin/root/mutation/business-update-map-info.ts +++ b/core/api/src/graphql/public/root/mutation/merchant-map-suggest.ts @@ -1,11 +1,11 @@ import { Merchants } from "@/app" import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map" import { GT } from "@/graphql/index" -import MerchantPayload from "@/graphql/admin/types/payload/merchant" +import MerchantPayload from "@/graphql/shared/types/payload/merchant" import Username from "@/graphql/shared/types/scalar/username" -const BusinessUpdateMapInfoInput = GT.Input({ - name: "BusinessUpdateMapInfoInput", +const MerchantMapSuggestInput = GT.Input({ + name: "MerchantMapSuggestInput", fields: () => ({ username: { type: GT.NonNull(Username), @@ -22,13 +22,13 @@ const BusinessUpdateMapInfoInput = GT.Input({ }), }) -const BusinessUpdateMapInfoMutation = GT.Field({ +const MerchantMapSuggestMutation = GT.Field({ extensions: { complexity: 120, }, type: GT.NonNull(MerchantPayload), args: { - input: { type: GT.NonNull(BusinessUpdateMapInfoInput) }, + input: { type: GT.NonNull(MerchantMapSuggestInput) }, }, resolve: async (_, args) => { const { username, title, latitude, longitude } = args.input @@ -44,7 +44,7 @@ const BusinessUpdateMapInfoMutation = GT.Field({ longitude, } - const merchant = await Merchants.updateBusinessMap({ + const merchant = await Merchants.suggestMerchantMap({ username, title, coordinates, @@ -61,4 +61,4 @@ const BusinessUpdateMapInfoMutation = GT.Field({ }, }) -export default BusinessUpdateMapInfoMutation +export default MerchantMapSuggestMutation diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index a11ad983eb8..d2b9bb76931 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -203,6 +203,13 @@ type BuildInformation { helmRevision: Int } +input BusinessMapSuggestInput { + latitude: Float! + longitude: Float! + title: String! + username: Username! +} + type CallbackEndpoint { id: EndpointId! url: EndpointUrl! @@ -778,6 +785,27 @@ type MapMarker { """Text field in a lightning payment transaction""" scalar Memo +type Merchant { + """ + GPS coordinates for the merchant that can be used to place the related business on a map + """ + coordinates: Coordinates + createdAt: Timestamp! + id: ID! + title: String! + + """The username of the merchant""" + username: Username! + + """Whether the merchant has been validated""" + validated: Boolean! +} + +type MerchantPayload { + errors: [Error!]! + merchant: Merchant +} + """(Positive) amount of minutes""" scalar Minutes @@ -795,6 +823,7 @@ type Mutation { accountEnableNotificationChannel(input: AccountEnableNotificationChannelInput!): AccountUpdateNotificationSettingsPayload! accountUpdateDefaultWalletId(input: AccountUpdateDefaultWalletIdInput!): AccountUpdateDefaultWalletIdPayload! accountUpdateDisplayCurrency(input: AccountUpdateDisplayCurrencyInput!): AccountUpdateDisplayCurrencyPayload! + businessMapSuggest(input: BusinessMapSuggestInput!): MerchantPayload! callbackEndpointAdd(input: CallbackEndpointAddInput!): CallbackEndpointAddPayload! callbackEndpointDelete(input: CallbackEndpointDeleteInput!): SuccessPayload! captchaCreateChallenge: CaptchaCreateChallengePayload! diff --git a/core/api/src/graphql/shared/types/object/merchant.ts b/core/api/src/graphql/shared/types/object/merchant.ts new file mode 100644 index 00000000000..2b72d835a87 --- /dev/null +++ b/core/api/src/graphql/shared/types/object/merchant.ts @@ -0,0 +1,33 @@ +import { GraphQLObjectType } from "graphql" + +import { GT } from "@/graphql/index" +import Coordinates from "@/graphql/shared/types/object/coordinates" +import Timestamp from "@/graphql/shared/types/scalar/timestamp" +import Username from "@/graphql/shared/types/scalar/username" + +const Merchant: GraphQLObjectType = GT.Object({ + name: "Merchant", + fields: () => ({ + id: { type: GT.NonNullID }, + title: { type: GT.NonNull(GT.String) }, + coordinates: { + type: Coordinates, + description: + "GPS coordinates for the merchant that can be used to place the related business on a map", + }, + validated: { + type: GT.NonNull(GT.Boolean), + description: "Whether the merchant has been validated", + }, + username: { + type: GT.NonNull(Username), + description: "The username of the merchant", + }, + createdAt: { + type: GT.NonNull(Timestamp), + resolve: (source) => source.createdAt, + }, + }), +}) + +export default Merchant diff --git a/core/api/src/graphql/admin/types/payload/merchant.ts b/core/api/src/graphql/shared/types/payload/merchant.ts similarity index 79% rename from core/api/src/graphql/admin/types/payload/merchant.ts rename to core/api/src/graphql/shared/types/payload/merchant.ts index e5c325c4d05..db674d70f55 100644 --- a/core/api/src/graphql/admin/types/payload/merchant.ts +++ b/core/api/src/graphql/shared/types/payload/merchant.ts @@ -1,4 +1,4 @@ -import AuditedMerchant from "../object/merchant" +import Merchant from "../object/merchant" import { GT } from "@/graphql/index" import IError from "@/graphql/shared/types/abstract/error" @@ -10,7 +10,7 @@ const MerchantPayload = GT.Object({ type: GT.NonNullList(IError), }, merchant: { - type: AuditedMerchant, + type: Merchant, }, }), }) diff --git a/core/api/src/services/mongoose/merchants.ts b/core/api/src/services/mongoose/merchants.ts index ce0e7376cd5..e8305088dae 100644 --- a/core/api/src/services/mongoose/merchants.ts +++ b/core/api/src/services/mongoose/merchants.ts @@ -8,6 +8,8 @@ import { interface IMerchantRepository { listForMap(): Promise + listPendingApproval(): Promise + findById(id: MerchantId): Promise findByUsername(username: Username): Promise create(args: { username: Username @@ -26,6 +28,20 @@ interface IMerchantRepository { } export const MerchantsRepository = (): IMerchantRepository => { + const findById = async ( + id: MerchantId, + ): Promise => { + try { + const result = await Merchant.findOne({ id }) + if (!result) { + return new CouldNotFindMerchantFromIdError(id) + } + return translateToMerchant(result) + } catch (err) { + return parseRepositoryError(err) + } + } + const findByUsername = async ( username: Username, ): Promise => { @@ -42,8 +58,18 @@ export const MerchantsRepository = (): IMerchantRepository => { const listForMap = async (): Promise => { try { - // only return merchants that have a location - const merchants = await Merchant.find({ location: { $exists: true } }) + const merchants = await Merchant.find({ validated: true }) + return merchants.map(translateToMerchant) + } catch (err) { + return parseRepositoryError(err) + } + } + + const listPendingApproval = async (): Promise< + BusinessMapMarker[] | RepositoryError + > => { + try { + const merchants = await Merchant.find({ validated: false }) return merchants.map(translateToMerchant) } catch (err) { return parseRepositoryError(err) @@ -130,6 +156,8 @@ export const MerchantsRepository = (): IMerchantRepository => { return { listForMap, + listPendingApproval, + findById, findByUsername, create, update, diff --git a/core/api/src/services/mongoose/schema.ts b/core/api/src/services/mongoose/schema.ts index e3fbe10d13f..32086469877 100644 --- a/core/api/src/services/mongoose/schema.ts +++ b/core/api/src/services/mongoose/schema.ts @@ -324,8 +324,7 @@ const MerchantSchema = new Schema({ location: { type: pointSchema, index: "2dsphere", - // for online commerce, we don't require coordinates - required: false, + required: true, }, createdAt: { type: Date, @@ -334,6 +333,7 @@ const MerchantSchema = new Schema({ validated: { type: Boolean, default: false, + index: true, }, }) diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index a865d539429..b9063cf3370 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -1035,6 +1035,40 @@ type MapMarker scalar Memo @join__type(graph: PUBLIC) +type Merchant + @join__type(graph: PUBLIC) +{ + """ + GPS coordinates for the merchant that can be used to place the related business on a map + """ + coordinates: Coordinates + createdAt: Timestamp! + id: ID! + title: String! + + """The username of the merchant""" + username: Username! + + """Whether the merchant has been validated""" + validated: Boolean! +} + +input MerchantMapSuggestInput + @join__type(graph: PUBLIC) +{ + latitude: Float! + longitude: Float! + title: String! + username: Username! +} + +type MerchantPayload + @join__type(graph: PUBLIC) +{ + errors: [Error!]! + merchant: Merchant +} + """(Positive) amount of minutes""" scalar Minutes @join__type(graph: PUBLIC) @@ -1168,6 +1202,7 @@ type Mutation """Sends a payment to a lightning address.""" lnurlPaymentSend(input: LnurlPaymentSendInput!): PaymentSendPayload! @join__field(graph: PUBLIC) + merchantMapSuggest(input: MerchantMapSuggestInput!): MerchantPayload! @join__field(graph: PUBLIC) onChainAddressCreate(input: OnChainAddressCreateInput!): OnChainAddressPayload! @join__field(graph: PUBLIC) onChainAddressCurrent(input: OnChainAddressCurrentInput!): OnChainAddressPayload! @join__field(graph: PUBLIC) onChainPaymentSend(input: OnChainPaymentSendInput!): PaymentSendPayload! @join__field(graph: PUBLIC)