From 17943bbf0f2f23171bd10888d717f67f8713fc5e Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 22 Oct 2026 02:30:45 +0000 Subject: [PATCH] store: name invoices answer 503 with names off --- .../src/StoreService/Web/Server.hs | 11 ++++++++--- tests/Bots/StoreService/NamesOrdersTests.hs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/simplex-store-service/src/StoreService/Web/Server.hs b/apps/simplex-store-service/src/StoreService/Web/Server.hs index 4ca52d0fd8..4b3e73f5d0 100644 --- a/apps/simplex-store-service/src/StoreService/Web/Server.hs +++ b/apps/simplex-store-service/src/StoreService/Web/Server.hs @@ -248,12 +248,15 @@ invoiceViewOf :: WebEnv -> InvoiceRow -> IO (Either Response J.Value) invoiceViewOf env@WebEnv {weConfig, weStore} row@InvoiceRow {irInvoiceId = InvoiceId invId, irProduct} = case irProduct of IPBadge badgeType months -> pure $ Right $ badgeInvoiceView (confirmationsFor weConfig) badgeType months row IPName rid -> case weOrders env of - Nothing -> Left internalError <$ logError ("invoice " <> invId <> " is a name's, and this service sells no names") + Nothing -> Left providerUnavailable <$ namesOff invId Just orders -> withTransaction weStore (`getRegistration` rid) >>= \case Nothing -> pure $ Left notFound Just r -> Right <$> nameInvoiceView orders (confirmationsFor weConfig) row r +namesOff :: Text -> IO () +namesOff invId = logError ("invoice " <> invId <> " is a name's, and this service sells no names") + -- | Monero reports a payment as confirming while its figures are zero, so the verdict counts as much as the figure. paymentMark :: InvoiceRow -> (Text, Bool) paymentMark InvoiceRow {irPayment} = @@ -261,11 +264,13 @@ paymentMark InvoiceRow {irPayment} = -- | The provider is cancelled first, because cancelling our record first would leave an address the buyer can still pay into with nothing watching it. cancelInvoiceHandler :: WebEnv -> InvoiceId -> Respond -> IO ResponseReceived -cancelInvoiceHandler env@WebEnv {weStore, weProviders} invId respond = +cancelInvoiceHandler env@WebEnv {weStore, weProviders} invId@(InvoiceId invText) respond = getInvoice weStore invId >>= \case Nothing -> respond notFound Just InvoiceRow {irStatus} | irStatus /= ISOpen -> respond (jsonError status409 "not_open") Just row | funded row -> respond (jsonError status409 "funded") + -- refused before the provider's cancel, since the answer could not show the order + Just InvoiceRow {irProduct = IPName _} | Nothing <- weOrders env -> namesOff invText >> respond providerUnavailable Just InvoiceRow {irProvider, irProviderRef} -> case providerNamed weProviders irProvider of Nothing -> respond providerUnavailable Just Provider {pCancelInvoice} -> @@ -279,7 +284,7 @@ cancelInvoiceHandler env@WebEnv {weStore, weProviders} invId respond = finish = do now <- getCurrentTime E.try (cancelOpenInvoice weStore (weWaiters env) (oePhases <$> weOrders env) invId now) >>= \case - Left (RegistrationChanged rid) -> maybe (pure internalError) (`registrationChangedResponse` rid) (weOrders env) >>= respond + Left (RegistrationChanged rid) -> maybe (pure providerUnavailable) (`registrationChangedResponse` rid) (weOrders env) >>= respond Right _ -> getInvoice weStore invId >>= maybe (respond notFound) (\row' -> respondInvoice env row' respond) paidParam :: Request -> Maybe (Text, Bool) diff --git a/tests/Bots/StoreService/NamesOrdersTests.hs b/tests/Bots/StoreService/NamesOrdersTests.hs index 31db1ec056..5dd7d1cf39 100644 --- a/tests/Bots/StoreService/NamesOrdersTests.hs +++ b/tests/Bots/StoreService/NamesOrdersTests.hs @@ -92,6 +92,7 @@ namesOrdersTests = describe "StoreService names: orders" $ do it "queues an order settled while open and publishes its version" testSettlementQueues it "closes the order of an invoice the provider closed or the sweep expired" testExpiryCloses it "cleans up unpaid orders after 30 days, reading the provider first" testCleanup + it "answers 503 for a name invoice while names are off, cancelling nothing" testNamesOff describe "invoice side" $ do it "finds an invoice by provider ref in either table, refusing a ref both hold" testProviderRefs it "reads and sweeps name invoices with the badge ones" testPollerQueries @@ -606,6 +607,21 @@ testCancelAnswers = withOrders $ \ctx -> do where cancelAt c (InvoiceId t) = post c ("/api/invoice/" <> T.unpack t <> "/cancel") "" +testNamesOff :: IO () +testNamesOff = withOrders $ \ctx -> do + (invId@(InvoiceId t), _) <- placed ctx aliceOrder + mgr <- newManager defaultManagerSettings + Warp.testWithApplication (pure $ webApp (cWeb ctx) {weOrders = Nothing}) $ \port -> do + let off = ctx {cClient = (mgr, "http://127.0.0.1:" <> show port)} + logs <- capturingStderr $ do + r <- webGet (cClient off) ("/api/invoice/" <> T.unpack t) + (statusOf r, J.decode (responseBody r)) `shouldBe` refusedWith 503 "provider_unavailable" + post off ("/api/invoice/" <> T.unpack t <> "/cancel") "" `shouldReturn` refusedWith 503 "provider_unavailable" + logs `shouldSatisfy` T.isInfixOf ("invoice " <> t <> " is a name's, and this service sells no names") + calls <- stubCalls (cStub ctx) + [c | c@StubCancel {} <- calls] `shouldBe` [] + (irStatus <$>) <$> getInvoice (cStore ctx) invId `shouldReturn` Just ISOpen + -- | The provider's cancel settles the invoice with the signal before it answers. withOrdersCancelling :: PaymentSignal -> (Ctx -> IO a) -> IO a withOrdersCancelling signal action = withOrders $ \ctx -> do