From cb05d169a27a48cd0f8805a49a5b250cb616ab2d Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 23 Jan 2026 17:13:19 +0000 Subject: [PATCH] wip --- .claude/settings.json | 28 +- .claude/skills/write-test/SKILL.md | 2 + conductor.json | 10 +- scripts/setup/setup.js | 10 +- scripts/setup/setupci.js | 8 +- scripts/setupTestUtils/setupPrompts.ts | 6 +- server/src/db/initDrizzle.ts | 10 +- server/src/external/redis/initUpstash.ts | 1 - server/src/honoMiddlewares/routeHandler.ts | 2 - .../src/internal/admin/handleGetOrgMember.ts | 5 +- .../attachToCheckPreview/getAttachScenario.ts | 8 +- .../balances/handlers/handleListBalances.ts | 126 +-- .../utils/deduction/logDeductionUpdates.ts | 2 +- .../deduction/prepareFeatureDeduction.ts | 8 +- .../src/internal/customers/CusReadService.ts | 4 +- .../internal/customers/CusSearchService.ts | 5 +- .../multiAttach/getAddAndRemoveProducts.ts | 14 +- .../multiAttach/mergeItemSets.ts | 2 +- .../upgradeDiffIntFlow/upgradeUtils.ts | 2 +- .../upgradeFlow/upgradeFlowUtils.ts | 2 +- .../upgradeSameIntFlow/comparePriceEntIds.ts | 3 +- .../getContUseDowngradeItems.ts | 41 +- .../attach/attachUtils/insertCustomItems.ts | 4 +- .../phaseUtils/mergeSimilarPhases.ts | 2 +- .../mergeUtils/phaseUtils/upsertNewPhase.ts | 2 +- .../customers/attach/models/AttachFlags.ts | 2 +- .../unifiedAttach/handleUnifiedAttach.ts | 4 +- .../cusProducts/cusEnts/RepService.ts | 8 +- .../cusProducts/cusEnts/cusEntHelpers.ts | 2 +- .../cusRollovers/getNewProductRollovers.ts | 12 +- .../apiCusCacheUtils/setCachedApiCustomer.ts | 8 +- .../setCachedGrantedBalance.ts | 7 +- .../src/internal/customers/getFullCusQuery.ts | 409 ++++----- .../handleDecreaseAndTransfer.ts | 4 +- .../customers/previews/checkProductUtils.ts | 38 +- .../customers/previews/previewUtils.ts | 12 +- .../src/internal/emails/sendMigrationEmail.ts | 12 +- server/src/internal/emails/sendOTPEmail.ts | 2 +- .../entityUtils/entityInvoiceUtils.ts | 12 +- .../entities/entityUtils/entityUtils.ts | 8 +- .../handleCreateEntity/entityToAction.ts | 13 +- .../internal/features/utils/displayUtils.ts | 4 +- .../handlers/handleGetStripeInvoice.ts | 2 +- .../invoiceItemUtils/invoiceItemUtils.ts | 13 +- server/src/internal/invoices/invoiceRouter.ts | 5 +- .../src/internal/migrations/migrationUtils.ts | 14 +- .../handleNukeOrganisationConfiguration.ts | 46 +- .../handlePushOrganisationConfiguration.ts | 148 ++-- .../savedViews/handlers/handleDeleteView.ts | 3 +- .../orgs/onboarding/parseChatProducts.ts | 6 +- .../internal/orgs/orgUtils/deleteOrgUtils.ts | 2 +- .../handlers/productActions/createProduct.ts | 5 +- .../getPrevAndNewUsages.ts | 12 +- .../prices/priceUtils/copyPriceUtils.ts | 15 +- .../prices/priceUtils/sortPriceUtils.ts | 6 +- .../productItemUtils/convertItem.ts | 4 +- .../productItemUtils/itemToPriceAndEnt.ts | 12 +- server/src/internal/products/productRouter.ts | 5 +- server/src/middleware/pricingMiddleware.ts | 15 +- server/src/middleware/publicAuthMiddleware.ts | 6 +- server/src/utils/authUtils.ts | 5 +- .../checkUtils/allCusProductsOnSubFree.ts | 2 +- .../src/utils/checkUtils/checkCusProducts.ts | 8 +- .../utils/checkUtils/checkSubCountMatch.ts | 3 - server/src/utils/checkUtils/saveCheckState.ts | 27 +- server/src/utils/encryptUtils.ts | 2 +- server/src/utils/initUtils.ts | 2 - server/src/utils/models/ItemSet.ts | 2 +- server/src/utils/scriptUtils/clearOrg.ts | 4 +- .../src/utils/scriptUtils/genScriptUtils.ts | 2 +- .../scriptUtils/getAll/getAllCusProds.ts | 10 +- .../utils/scriptUtils/getAll/getAllUsers.ts | 3 +- .../src/utils/scriptUtils/resetOrgStripe.ts | 4 +- server/src/utils/timeUtils.ts | 6 +- .../customInterval/customInterval1.test.ts | 6 +- .../customInterval/customInterval3.test.ts | 6 +- .../customInterval/customInterval4.test.ts | 6 +- .../multiFeature/multiFeature1.test.ts | 2 +- .../multiFeature/multiFeature2.test.ts | 2 +- .../multiFeature/multiFeature3.test.ts | 4 +- server/tests/advanced/usage/sharedProducts.ts | 2 +- server/tests/advanced/usage/usage1.test.ts | 4 +- .../advanced/usageLimit/usageLimit1.test.ts | 2 +- .../advanced/usageLimit/usageLimit2.test.ts | 4 +- .../advanced/usageLimit/usageLimit3.test.ts | 4 +- .../contUse/update/updateContUse1.test.ts | 10 +- .../contUse/update/updateContUse3.test.ts | 6 +- .../contUse/update/updateContUse4.test.ts | 8 +- .../contUse/update/updateContUse5.test.ts | 8 +- server/tests/attach/addOn/addOn2.test.ts | 4 +- .../tests/attach/checkout/checkout1.test.ts | 2 +- .../tests/attach/checkout/checkout8.test.ts | 2 +- .../tests/attach/downgrade/downgrade2.test.ts | 4 +- .../attach/migrations/runMigrationTest.ts | 28 +- .../attach/newVersion/newVersion2.test.ts | 10 +- server/tests/attach/others/others1.test.ts | 12 +- server/tests/attach/others/others10.test.ts | 132 +-- server/tests/attach/others/others2.test.ts | 2 +- server/tests/attach/others/others3.test.ts | 8 +- server/tests/attach/others/others7.test.ts | 12 +- server/tests/attach/others/others8.test.ts | 12 +- server/tests/attach/others/others9.test.ts | 12 +- .../attach/updateEnts/updateEnts4.test.ts | 10 +- .../updateQuantity/updateQuantity1.test.ts | 6 +- server/tests/attach/upgrade/upgrade4.test.ts | 6 +- server/tests/attach/upgrade/upgrade5.test.ts | 6 +- .../attach/upgradeOld/upgradeOld3.test.ts | 6 +- .../attach/upgradeOld/upgradeOld4.test.ts | 4 +- .../loose/entities/entity-loose-4.test.ts | 6 +- .../loose/entities/entity-loose-6.test.ts | 4 +- .../balances/check/loose/loose-3.test.ts | 4 +- .../balances/check/loose/loose-4.test.ts | 7 +- .../check/prepaid/check-prepaid2.test.ts | 3 +- .../track/allocated/track-allocated4.test.ts | 2 +- .../balances/track/basic/track-basic2.test.ts | 2 +- ...akdown-customer-and-entity-product.test.ts | 15 +- .../track-breakdown-mixed-entity.test.ts | 17 +- .../track/breakdown/track-breakdown5.test.ts | 1 - .../concurrency/concurrent-track5.test.ts | 4 +- .../concurrency/concurrent-track6.test.ts | 4 +- .../track-entity-products3.test.ts | 2 +- .../track/legacy/track-legacy1.test.ts | 4 +- .../balances/track/misc/track-misc1.test.ts | 2 +- .../balances/track/misc/track-misc2.test.ts | 2 +- .../balances/track/misc/track-misc3.test.ts | 2 +- .../update/legacy-balance-update1.test.ts | 98 +-- .../update-combined/update-combined3.test.ts | 1 - .../basic/update-current-balance2.test.ts | 1 - .../basic/update-current-balance5.test.ts | 10 +- .../revenuecat/revenuecatWebhooks.test.ts | 628 +++++++------- .../balances/check/check-public-key.test.ts | 179 ++-- .../cancel/add-ons/cancel-addon1.test.ts | 4 +- .../cancel/add-ons/cancel-addon4.test.ts | 16 +- .../invoice-action-required2.test.ts | 6 +- .../invoice-action-required3.test.ts | 3 +- .../invoice-action-required4.test.ts | 12 +- .../new-subscription-action-required1.test.ts | 2 +- .../new-billing-subscription1.test.ts | 2 +- .../billing/renew/renew-addon1.test.ts | 6 +- ...cancel-end-of-cycle-trial-entities.test.ts | 299 ++++++- .../cancel-end-of-cycle-trial.test.ts | 13 +- .../discounts/amount-off-discount.test.ts | 668 +++++++-------- .../discounts/discount-applies-to.test.ts | 675 ++++++++------- .../discounts/percent-off-discount.test.ts | 799 +++++++++--------- .../update-quantity-invoice-mode.test.ts | 654 +++++++------- .../update-quantity/increase-quantity.test.ts | 471 +++++------ .../update-quantity/invoicing.test.ts | 93 +- .../no-change-quantity.test.ts | 91 +- .../quantity-stripe-sync.test.ts | 330 ++++---- .../utils/discounts/discountTestUtils.ts | 10 +- .../utils/expectCustomerProductCorrect.ts | 45 +- .../multiSub/multiSubInterval3.test.ts | 4 +- server/tests/merged/add/mergedAdd3.test.ts | 4 +- .../merged/downgrade/mergedDowngrade1.test.ts | 4 +- .../merged/downgrade/mergedDowngrade6.test.ts | 4 +- .../merged/downgrade/mergedDowngrade8.test.ts | 4 +- .../tests/merged/separate/separate1.test.ts | 2 +- .../stripe/discounts/sub-to-discounts.spec.ts | 4 +- ...ription-items-update-multi-product.spec.ts | 2 +- .../cusProductUtils/cusEntSearchUtils.ts | 19 +- .../expectAttach/expectResetAtCorrect.ts | 2 +- .../expectAttach/expectTrialEndsAt.ts | 12 +- .../expectContUse/expectEntityUtils.ts | 14 +- .../expectContUse/expectUpdateContUse.ts | 6 +- server/tests/utils/fixtures/db/contexts.ts | 4 +- server/tests/utils/fixtures/db/entities.ts | 4 - server/tests/utils/fixtures/db/rollovers.ts | 6 +- shared/api/balances/check/checkParams.ts | 3 +- .../balances/create/createBalanceParams.ts | 8 +- shared/api/core/checkProductModels.ts | 123 ++- shared/api/features/featureV1OpModels.ts | 90 +- shared/api/others/apiDiscount.ts | 9 +- shared/db/utils.ts | 3 +- shared/models/analyticsModels/actionTable.ts | 8 +- .../attachModels/attachEnums/AttachConfig.ts | 2 +- shared/models/authModels/membership.ts | 2 +- .../chatResultModels/chatResultTable.ts | 7 +- shared/models/cusModels/cusRelations.ts | 4 +- .../cusModels/entityModels/entityRelations.ts | 4 +- .../invoiceModels/invoiceRelations.ts | 2 +- .../cusEntModels/replaceableRelations.ts | 2 +- .../cusPriceModels/cusPriceRelations.ts | 2 +- .../cusPriceModels/cusPriceTable.ts | 10 +- .../cusProductModels/cusProductRelations.ts | 4 +- shared/models/devModels/apiKeyModels.ts | 2 +- shared/models/devModels/apiKeyTable.ts | 10 +- shared/models/eventModels/eventTable.ts | 14 +- .../migrationErrorRelations.ts | 2 +- .../migrationModels/migrationJobTable.ts | 4 +- .../productModels/entModels/entRelations.ts | 2 +- .../freeTrialModels/freeTrialRelations.ts | 2 +- .../priceModels/priceRelations.ts | 2 +- .../productModels/priceModels/priceTable.ts | 21 +- .../referralModels/referralCodeRelations.ts | 2 +- .../referralModels/referralCodeTable.ts | 6 +- .../rewardRedemptionRelations.ts | 4 +- .../referralModels/rewardRedemptionTable.ts | 6 +- .../rewardProgramRelations.ts | 2 +- .../rewardProgramModels/rewardProgramTable.ts | 11 +- shared/models/subModels/subTable.ts | 10 +- .../lineItemUtils/priceToLineAmount.ts | 52 +- .../applyProrationToLineItem.ts | 1 - .../prorationUtils/prorationConfigUtils.ts | 2 +- .../utils/cusEntUtils/convertCusEntUtils.ts | 2 +- .../findCustomerEntitlementByFeature.ts | 5 +- shared/utils/cusPriceUtils/index.ts | 3 +- .../utils/featureUtils/convertFeatureUtils.ts | 18 +- vite/eslint.config.js | 2 +- vite/src/components/ai-elements/artifact.tsx | 188 ++--- vite/src/components/ai-elements/canvas.tsx | 26 +- .../ai-elements/chain-of-thought.tsx | 324 +++---- .../src/components/ai-elements/checkpoint.tsx | 91 +- .../src/components/ai-elements/code-block.tsx | 274 +++--- .../components/ai-elements/confirmation.tsx | 222 ++--- .../src/components/ai-elements/connection.tsx | 42 +- vite/src/components/ai-elements/context.tsx | 596 ++++++------- vite/src/components/ai-elements/controls.tsx | 18 +- .../components/ai-elements/conversation.tsx | 134 +-- vite/src/components/ai-elements/edge.tsx | 216 ++--- vite/src/components/ai-elements/image.tsx | 32 +- .../ai-elements/inline-citation.tsx | 368 ++++---- vite/src/components/ai-elements/loader.tsx | 166 ++-- vite/src/components/ai-elements/message.tsx | 645 +++++++------- .../components/ai-elements/model-selector.tsx | 242 +++--- vite/src/components/ai-elements/node.tsx | 70 +- .../components/ai-elements/open-in-chat.tsx | 582 ++++++------- vite/src/components/ai-elements/panel.tsx | 16 +- vite/src/components/ai-elements/plan.tsx | 158 ++-- vite/src/components/ai-elements/queue.tsx | 320 +++---- vite/src/components/ai-elements/reasoning.tsx | 265 +++--- vite/src/components/ai-elements/shimmer.tsx | 96 +-- vite/src/components/ai-elements/sources.tsx | 100 +-- .../src/components/ai-elements/suggestion.tsx | 75 +- vite/src/components/ai-elements/task.tsx | 106 +-- vite/src/components/ai-elements/tool.tsx | 238 +++--- vite/src/components/ai-elements/toolbar.tsx | 18 +- .../components/ai-elements/web-preview.tsx | 402 ++++----- vite/src/components/autumn/attach-dialog.tsx | 11 +- .../src/components/autumn/checkout-dialog.tsx | 25 +- vite/src/components/general/CopyablePre.tsx | 7 +- .../src/components/general/OnboardingStep.tsx | 4 +- vite/src/components/general/SideAccordion.tsx | 2 +- vite/src/components/general/SmallSpinner.tsx | 13 +- vite/src/components/general/ToggleButton.tsx | 4 +- .../table-components/HamburgerMenu.tsx | 6 +- .../table-components/ToolbarButton.tsx | 6 +- .../src/components/pricing/pricing-dialog.tsx | 8 +- vite/src/components/ui/accordion.tsx | 2 +- vite/src/components/ui/alert.tsx | 2 +- vite/src/components/ui/badge.tsx | 2 +- vite/src/components/ui/border-beam.tsx | 196 ++--- vite/src/components/ui/breadcrumb.tsx | 2 +- vite/src/components/ui/button-group.tsx | 129 ++- vite/src/components/ui/calendar.tsx | 5 +- vite/src/components/ui/card.tsx | 2 +- vite/src/components/ui/carousel.tsx | 391 +++++---- vite/src/components/ui/collapsible.tsx | 36 +- vite/src/components/ui/context-menu.tsx | 2 +- vite/src/components/ui/drawer.tsx | 2 +- vite/src/components/ui/hover-card.tsx | 56 +- vite/src/components/ui/input-otp.tsx | 2 +- vite/src/components/ui/pagination.tsx | 5 +- vite/src/components/ui/progress.tsx | 46 +- vite/src/components/ui/scroll-area.tsx | 2 +- vite/src/components/ui/select.tsx | 2 +- vite/src/components/ui/shine-border.tsx | 96 +-- vite/src/components/ui/sonner.tsx | 2 +- vite/src/components/ui/switch.tsx | 2 +- vite/src/components/ui/textarea.tsx | 26 +- .../components/v2/dropdowns/DropdownMenu.tsx | 762 ++++++++--------- vite/src/contexts/PortalContainerContext.tsx | 5 +- .../hooks/common/useGlobalErrorHandler.tsx | 2 +- .../hooks/queries/revcat/useRCProducts.tsx | 7 +- vite/src/hooks/queries/useInvitesQuery.tsx | 4 +- vite/src/hooks/stores/useRewardStore.ts | 6 +- vite/src/hooks/stores/useSDKStore.ts | 1 - vite/src/hooks/stores/useSecretKeyStore.ts | 1 - vite/src/hooks/useDropdownShortcut.ts | 3 - vite/src/lib/auth-client.ts | 7 +- vite/src/lib/autumn/attach-content.tsx | 2 +- .../lib/autumn/get-product-change-texts.tsx | 2 +- vite/src/lib/shikiHighlighter.ts | 1 - vite/src/lib/snippets/curlSnippets.ts | 3 +- vite/src/lib/utils.ts | 2 +- vite/src/services/CreditService.tsx | 2 +- vite/src/services/DevService.tsx | 2 +- vite/src/services/OrgService.tsx | 2 +- vite/src/services/products/PriceService.tsx | 2 +- .../products/RewardProgramService.tsx | 4 +- vite/src/services/useAxiosSwr.tsx | 6 +- vite/src/styles/form/effects.css | 4 +- vite/src/utils/formatUtils/formatDateUtils.ts | 1 + vite/src/utils/product/getItemType.ts | 2 +- vite/src/utils/product/priceUtils.ts | 11 +- .../product/productItemUtils/convertItem.ts | 4 +- vite/src/utils/productUtils.ts | 8 +- vite/src/views/admin/AdminTable.tsx | 5 +- vite/src/views/admin/AdminView.tsx | 12 +- vite/src/views/admin/DataTable.tsx | 20 +- vite/src/views/admin/adminUtils.ts | 11 +- .../src/views/admin/components/CreateUser.tsx | 10 +- .../views/admin/components/ImpersonateBtn.tsx | 4 +- vite/src/views/admin/orgColumns.tsx | 6 +- vite/src/views/admin/userColumns.tsx | 8 +- vite/src/views/auth/AcceptInvitation.tsx | 2 +- vite/src/views/command-bar/CommandBar.tsx | 10 +- vite/src/views/command-bar/command-row.tsx | 8 +- .../components/CustomerRowToolbar.tsx | 14 +- .../components/CustomersPagination.tsx | 4 +- .../components/CustomersSearchBar.tsx | 4 +- .../customers/components/CustomersTable.tsx | 20 +- .../customers/components/StatusBadge.tsx | 2 +- .../customers-table/CustomerRow.tsx | 37 +- .../customers-top-bar/CustomersTopBar.tsx | 6 +- .../filter-dropdown/SavedViewPopover.tsx | 224 +++-- .../customers/customer/InvoicesTable.tsx | 6 +- .../customer/analytics/ViewUserEvents.tsx | 4 +- .../components/SelectGroupByDropdown.tsx | 1 - .../analytics/hooks/useTopEventNames.tsx | 2 +- .../analytics/utils/extractPropertyKeys.ts | 3 - .../components/CusProductEntityItem.tsx | 4 +- .../customer/components/EntityHeader.tsx | 5 +- .../add-coupon/AddCouponDialogContent.tsx | 28 +- .../customer-header/CustomerBreadcrumbs.tsx | 4 +- .../customer-header/SelectEntity.tsx | 16 +- .../customer-sidebar/CustomerEntities.tsx | 10 +- .../create-entity/CreateEntity.tsx | 18 +- .../create-entity/EntityConfig.tsx | 4 +- .../CusProductToolbar.tsx | 12 +- .../TransferProductDialog.tsx | 9 +- .../UpdateStatusDropdownBtn.tsx | 12 +- .../customer/hooks/useCachedCustomer.tsx | 2 +- .../components/AttachPreviewDetails.tsx | 12 +- .../components/CustomerProductBreadcrumbs.tsx | 4 +- .../components/InvoiceCustomerButton.tsx | 6 +- .../product/components/ToggleConfigButton.tsx | 2 +- .../components/attach-preview/AttachInfo.tsx | 10 +- .../customer/product/hooks/useAttachState.tsx | 13 +- .../product/multi-attach/MultiAttachLines.tsx | 7 +- .../product/utils/getCusProductMinQuantity.ts | 2 +- .../CustomerProductsStatus.tsx | 4 +- .../customer-plan/CustomerPlanEditorBar.tsx | 4 +- .../customers2/customer/CustomerSheets.tsx | 32 +- .../components/CreateApiKeyDialog.tsx | 14 +- vite/src/views/general/CloseScreen.tsx | 18 +- vite/src/views/general/ErrorScreen.tsx | 13 +- .../notifications/InviteNotifications.tsx | 344 ++++---- .../components/AdminDropdownItems.tsx | 8 +- .../main-sidebar/components/LogOutItem.tsx | 7 +- .../main-sidebar/components/ManageOrg.tsx | 156 ++-- .../main-sidebar/components/OrgDetails.tsx | 232 ++--- .../main-sidebar/components/UserButton.tsx | 6 +- .../main-sidebar/components/UserDetails.tsx | 142 ++-- .../org-dropdown/hooks/useMemberships.tsx | 6 +- .../manage-org/DeleteOrgPopover.tsx | 206 ++--- .../org-dropdown/manage-org/InvitePopover.tsx | 172 ++-- .../manage-org/LeaveOrgPopover.tsx | 182 ++-- .../manage-org/MemberRowToolbar.tsx | 12 +- .../manage-org/OrgInvitesList.tsx | 128 +-- .../manage-org/OrgMembersList.tsx | 106 ++- vite/src/views/onboarding/OnboardingView.tsx | 37 +- .../views/onboarding/components/CodeBlock.tsx | 3 +- .../onboarding/components/OptionButton.tsx | 2 +- .../views/onboarding/hooks/useCreateOrg.tsx | 4 +- .../onboarding-steps/AttachProduct.tsx | 8 +- .../onboarding-steps/AutumnProvider.tsx | 2 +- .../onboarding-steps/CheckAccess.tsx | 9 +- .../onboarding-steps/ConnectStripe.tsx | 11 +- .../onboarding/onboarding-steps/CreateOrg.tsx | 6 +- .../onboarding-steps/CreateSecretKey.tsx | 15 +- .../views/onboarding/onboarding-steps/Env.tsx | 3 +- .../onboarding/onboarding-steps/Install.tsx | 3 +- .../onboarding-steps/IntegrationGuide.tsx | 2 +- .../onboarding-steps/MountHandler.tsx | 4 +- .../onboarding/onboarding-steps/SampleApp.tsx | 41 +- vite/src/views/onboarding2/AttachProduct.tsx | 8 +- vite/src/views/onboarding2/AutumnProvider.tsx | 4 +- vite/src/views/onboarding2/Env.tsx | 3 +- vite/src/views/onboarding2/Install.tsx | 3 +- vite/src/views/onboarding2/MountHandler.tsx | 4 +- .../src/views/onboarding2/OnboardingView2.tsx | 18 +- vite/src/views/onboarding2/Step.tsx | 4 +- .../views/onboarding2/integrate/AITools.tsx | 14 +- .../integrate/ConnectStripeStep.tsx | 8 +- .../onboarding2/integrate/IntegrateAutumn.tsx | 24 +- .../views/onboarding2/integrate/NextSteps.tsx | 2 +- .../onboarding2/integrate/SelectStack.tsx | 9 +- .../integration-steps/AddAutumnProvider.tsx | 5 +- .../integration-steps/AutumnHandler.tsx | 43 +- .../integration-steps/CreateSecretKey.tsx | 9 +- .../integrate/integration-steps/EnvStep.tsx | 2 +- .../model-pricing/AddTrialButton.tsx | 10 +- .../model-pricing/ModelPricing.tsx | 43 +- .../model-pricing-utils/modelPricingUtils.ts | 6 +- .../views/onboarding2/utils/connectStripe.ts | 4 +- .../onboarding2/utils/useCustomerReplica.tsx | 2 +- .../integration-step/ConnectStripeSection.tsx | 4 +- .../playground-step/PlaygroundToolbar.tsx | 5 +- .../hooks/actions/useStepActions.tsx | 4 +- .../hooks/useSyncPlaygroundMode.tsx | 8 +- .../onboarding4/preview/PreviewPlanCard.tsx | 18 +- .../components/CreateFeatureFooter.tsx | 4 +- .../components/UpdateCreditSystemSheet.tsx | 5 +- .../utils/validateCreditSystem.ts | 2 +- .../feature-list/FeatureListCreateButton.tsx | 2 - .../feature-list/FeatureListMenuButton.tsx | 1 - .../feature-list/FeatureListRowToolbar.tsx | 3 +- .../views/products/hooks/useProductTable.ts | 1 - .../plan/components/EditPlanPriceSheet.tsx | 2 +- .../plan/components/EditPlanSheet.tsx | 2 +- .../plan/components/SaveChangesBar.tsx | 10 +- .../new-feature/NewFeatureSheet.tsx | 5 +- .../plan/hooks/useProductChangedAlert.tsx | 2 +- .../views/products/product/ProductSidebar.tsx | 4 +- .../products/product/ProductVersions.tsx | 8 +- .../components/ProductViewBreadcrumbs.tsx | 9 +- .../components/UpdateProductButton.tsx | 10 +- .../product/free-trial/CreateFreeTrial.tsx | 21 +- .../product/hooks/NavigationBlockerModal.tsx | 4 +- .../product/hooks/getCachedProduct.tsx | 2 +- .../product/hooks/useProductChangedAlert.tsx | 4 +- .../product/prices/CreateFixedPrice.tsx | 26 +- .../products/product/prices/PricingConfig.tsx | 2 +- .../product/prices/PricingTypeBadge.tsx | 2 +- .../product/product-item/EntitiesDropdown.tsx | 20 +- .../product-item/ProductItemConfig.tsx | 12 +- .../components/ConfigWithFeature.tsx | 18 +- .../components/SelectItemFeature.tsx | 19 +- .../CreateItemDialogContent.tsx | 2 +- .../create-product-item/defaultItemConfigs.ts | 4 +- .../AdvancedConfigSidebar.tsx | 2 +- .../product-item-config/PriceItemConfig.tsx | 12 +- .../advanced-config/AdvancedItemConfig.tsx | 17 +- .../proration-config/OnDecreaseSelect.tsx | 2 +- .../proration-config/ProrationSelect.tsx | 2 +- .../components/CusomiseIntervalPopover.tsx | 10 +- .../components/SelectFeature.tsx | 11 +- .../components/feature-price/BillingUnits.tsx | 11 +- .../feature-price/PrepaidToggle.tsx | 6 +- .../feature-price/SelectBillingCycle.tsx | 8 +- .../feature-price/UsageTierInput.tsx | 2 +- .../AddToEntityDropdown.tsx | 2 +- .../item-config-footer/ItemConfigFooter.tsx | 14 +- .../utils/getFrontendItemConfig.tsx | 2 +- .../product-sidebar/ToggleDefaultProduct.tsx | 18 +- .../versioning/ConfirmMigrateDialog.tsx | 10 +- .../versioning/ConfirmNewVersionDialog.tsx | 14 +- .../product/versioning/CountAndMigrate.tsx | 26 +- .../product-row-toolbar/ProductTypeBadge.tsx | 2 +- .../rewards/components/RewardRowToolbar.tsx | 26 +- .../components/ProductPriceSelector.tsx | 5 +- .../RewardProgramRowToolbar.tsx | 17 +- .../RewardProgramsTableColumns.tsx | 1 - .../products/rewards/utils/rewardMappers.ts | 4 +- 454 files changed, 9436 insertions(+), 9160 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 3c9d8b4a8..b168f0647 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,15 +1,15 @@ { - "hooks": { - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "npx ultracite fix" - } - ] - } - ] - } -} \ No newline at end of file + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "npx ultracite fix" + } + ] + } + ] + } +} diff --git a/.claude/skills/write-test/SKILL.md b/.claude/skills/write-test/SKILL.md index 9160ec341..9ac3bdd06 100644 --- a/.claude/skills/write-test/SKILL.md +++ b/.claude/skills/write-test/SKILL.md @@ -26,6 +26,7 @@ Write integration tests for the Autumn billing system using the `initScenario` p - Use `Decimal.js` for balance calculations in track tests - Unique `customerId` per test - Use generic types with `AutumnInt`: `autumnV1.customers.get()`, `autumnV1.check()` +- **USE UTILITY FUNCTIONS WHENEVER POSSIBLE** - the shorter the code, the better. Check `server/tests/integration/billing/utils/` for existing utilities like `expectCustomerProducts`, `expectProductScheduled`, `expectCustomerInvoiceCorrect`, etc. **DON'T:** - Use `describe/beforeAll/test` (legacy pattern) @@ -33,6 +34,7 @@ Write integration tests for the Autumn billing system using the `initScenario` p - Share state between tests - Use raw arithmetic for balance calculations (floating point errors) - Use `as unknown as Type` casting - use generic types instead +- Write manual assertion loops when a utility function exists ## AutumnInt Response Types diff --git a/conductor.json b/conductor.json index 839cdcfa4..79be7976a 100644 --- a/conductor.json +++ b/conductor.json @@ -1,6 +1,6 @@ { - "scripts": { - "setup": "zsh conductor-setup.sh", - "run": "bun run dev:bun" - } -} \ No newline at end of file + "scripts": { + "setup": "zsh conductor-setup.sh", + "run": "bun run dev:bun" + } +} diff --git a/scripts/setup/setup.js b/scripts/setup/setup.js index ead02880d..89442fac2 100644 --- a/scripts/setup/setup.js +++ b/scripts/setup/setup.js @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { randomBytes } from "crypto"; -import { writeFileSync, copyFileSync, readFileSync } from "fs"; -import inquirer from "inquirer"; -import { spawnSync } from "child_process"; import chalk from "chalk"; +import { spawnSync } from "child_process"; +import { randomBytes } from "crypto"; +import { copyFileSync, readFileSync, writeFileSync } from "fs"; +import inquirer from "inquirer"; const genUrlSafeBase64 = (bytes) => { return randomBytes(bytes) @@ -253,7 +253,7 @@ async function main() { }; let databaseUrl = ""; - let stripeWebhookVars = []; + const stripeWebhookVars = []; databaseUrl = await handleDatabaseSetup(); // stripeWebhookVars = await handleLocalRunSetup(); diff --git a/scripts/setup/setupci.js b/scripts/setup/setupci.js index 92c2d7fd9..514f14bd9 100644 --- a/scripts/setup/setupci.js +++ b/scripts/setup/setupci.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { randomBytes } from "crypto"; -import { writeFileSync, copyFileSync } from "fs"; import chalk from "chalk"; +import { randomBytes } from "crypto"; +import { copyFileSync, writeFileSync } from "fs"; const genUrlSafeBase64 = (bytes) => { return randomBytes(bytes) @@ -26,8 +26,8 @@ async function main() { STRIPE_WEBHOOK_URL: process.env.STRIPE_WEBHOOK_URL, }; - let databaseUrl = process.env.DATABASE_URL; - let stripeWebhookVars = []; + const databaseUrl = process.env.DATABASE_URL; + const stripeWebhookVars = []; // stripeWebhookVars = await handleLocalRunSetup(); // Step 11: Write to server/.env diff --git a/scripts/setupTestUtils/setupPrompts.ts b/scripts/setupTestUtils/setupPrompts.ts index eb73214db..49902a733 100644 --- a/scripts/setupTestUtils/setupPrompts.ts +++ b/scripts/setupTestUtils/setupPrompts.ts @@ -1,5 +1,5 @@ -import inquirer from "inquirer"; import chalk from "chalk"; +import inquirer from "inquirer"; /** * Prompts user for Stripe test API key @@ -111,7 +111,9 @@ export async function setupTunnelUrl(): Promise { ), ); console.log( - chalk.cyan("You can use tools like ngrok, localtunnel, or Cloudflare Tunnel."), + chalk.cyan( + "You can use tools like ngrok, localtunnel, or Cloudflare Tunnel.", + ), ); console.log(chalk.cyan("Example: https://your-subdomain.ngrok.io\n")); diff --git a/server/src/db/initDrizzle.ts b/server/src/db/initDrizzle.ts index 607c29b3c..86baafafc 100644 --- a/server/src/db/initDrizzle.ts +++ b/server/src/db/initDrizzle.ts @@ -9,9 +9,15 @@ import postgres from "postgres"; export const client = postgres(process.env.DATABASE_URL!); export const db = drizzle(client, { schema }); -export const initDrizzle = (params?: { maxConnections?: number, replica?: boolean }) => { +export const initDrizzle = (params?: { + maxConnections?: number; + replica?: boolean; +}) => { const maxConnections = params?.maxConnections || 10; - const dbUrl = (params?.replica ? process.env.DATABASE_REPLICA_URL : process.env.DATABASE_URL) ?? ""; + const dbUrl = + (params?.replica + ? process.env.DATABASE_REPLICA_URL + : process.env.DATABASE_URL) ?? ""; const client = postgres(dbUrl, { max: maxConnections, }); diff --git a/server/src/external/redis/initUpstash.ts b/server/src/external/redis/initUpstash.ts index 276b8bbce..0a6a7aeb0 100644 --- a/server/src/external/redis/initUpstash.ts +++ b/server/src/external/redis/initUpstash.ts @@ -1,6 +1,5 @@ import { Redis } from "@upstash/redis"; - const upstash = new Redis({ url: process.env.CLOUD_UPSTASH_REDIS_REST_URL, token: process.env.CLOUD_UPSTASH_REDIS_REST_TOKEN, diff --git a/server/src/honoMiddlewares/routeHandler.ts b/server/src/honoMiddlewares/routeHandler.ts index 3a4702b04..70b1fd896 100644 --- a/server/src/honoMiddlewares/routeHandler.ts +++ b/server/src/honoMiddlewares/routeHandler.ts @@ -124,7 +124,6 @@ export function createRoute< middlewares.push(validator("json", opts.body)); } - // Same for query if (opts.versionedQuery && opts.resource) { middlewares.push( @@ -143,7 +142,6 @@ export function createRoute< middlewares.push(validator("param", opts.params)); } - // Add expand middleware after query validation if (opts.query || opts.versionedQuery) { middlewares.push(expandMiddleware()); diff --git a/server/src/internal/admin/handleGetOrgMember.ts b/server/src/internal/admin/handleGetOrgMember.ts index 88317788b..c0c293ff1 100644 --- a/server/src/internal/admin/handleGetOrgMember.ts +++ b/server/src/internal/admin/handleGetOrgMember.ts @@ -4,10 +4,10 @@ import { createRoute } from "../../honoMiddlewares/routeHandler"; /** * GET /admin/org-member - * + * * Query params: * - org_id: The org ID to get a member for - * + * * Returns the userId of the first member of the org. */ export const handleGetOrgMember = createRoute({ @@ -32,4 +32,3 @@ export const handleGetOrgMember = createRoute({ return c.json({ userId: orgMember.userId }); }, }); - diff --git a/server/src/internal/api/check/handlers/attachToCheckPreview/getAttachScenario.ts b/server/src/internal/api/check/handlers/attachToCheckPreview/getAttachScenario.ts index 8aa472fab..678ef1c15 100644 --- a/server/src/internal/api/check/handlers/attachToCheckPreview/getAttachScenario.ts +++ b/server/src/internal/api/check/handlers/attachToCheckPreview/getAttachScenario.ts @@ -1,10 +1,10 @@ -import { isFreeProduct } from "@/internal/products/productUtils.js"; import { AttachBranch, - AttachPreview, + type AttachPreview, AttachScenario, - FullProduct, + type FullProduct, } from "@autumn/shared"; +import { isFreeProduct } from "@/internal/products/productUtils.js"; export const getAttachScenario = async ({ preview, @@ -13,7 +13,7 @@ export const getAttachScenario = async ({ preview: AttachPreview; product: FullProduct; }) => { - let branch = preview.branch; + const branch = preview.branch; if ( branch == AttachBranch.New || diff --git a/server/src/internal/balances/handlers/handleListBalances.ts b/server/src/internal/balances/handlers/handleListBalances.ts index f7fdf7869..2127a157d 100644 --- a/server/src/internal/balances/handlers/handleListBalances.ts +++ b/server/src/internal/balances/handlers/handleListBalances.ts @@ -1,8 +1,8 @@ import { - customerEntitlements, - customerProducts, - entitlements, - features, + customerEntitlements, + customerProducts, + entitlements, + features, } from "@autumn/shared"; import { CustomerNotFoundError } from "@shared/index"; import { and, eq, isNull } from "drizzle-orm"; @@ -11,71 +11,71 @@ import { createRoute } from "@/honoMiddlewares/routeHandler"; import { CusService } from "@/internal/customers/CusService"; const ListBalancesSchema = z.object({ - customer_id: z.string(), + customer_id: z.string(), }); export const handleListBalances = createRoute({ - query: ListBalancesSchema, - handler: async (c) => { - const ctx = c.get("ctx"); - const { customer_id } = c.req.valid("query"); + query: ListBalancesSchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const { customer_id } = c.req.valid("query"); - const fullCus = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customer_id, - orgId: ctx.org.id, - env: ctx.env, - }); + const fullCus = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customer_id, + orgId: ctx.org.id, + env: ctx.env, + }); - if (!fullCus) { - throw new CustomerNotFoundError({ customerId: customer_id }); - } + if (!fullCus) { + throw new CustomerNotFoundError({ customerId: customer_id }); + } - // Get customer entitlements where the entitlement has no internal_product_id - const rawBalances = await ctx.db - .select({ - customer_entitlement: customerEntitlements, - entitlement: entitlements, - feature: features, - customer_product: customerProducts, - }) - .from(customerEntitlements) - .innerJoin( - entitlements, - eq(customerEntitlements.entitlement_id, entitlements.id), - ) - .innerJoin( - features, - eq(entitlements.internal_feature_id, features.internal_id), - ) - .leftJoin( - customerProducts, - eq(customerEntitlements.customer_product_id, customerProducts.id), - ) - .where( - and( - eq(customerEntitlements.internal_customer_id, fullCus.internal_id), - isNull(entitlements.internal_product_id), - ), - ); + // Get customer entitlements where the entitlement has no internal_product_id + const rawBalances = await ctx.db + .select({ + customer_entitlement: customerEntitlements, + entitlement: entitlements, + feature: features, + customer_product: customerProducts, + }) + .from(customerEntitlements) + .innerJoin( + entitlements, + eq(customerEntitlements.entitlement_id, entitlements.id), + ) + .innerJoin( + features, + eq(entitlements.internal_feature_id, features.internal_id), + ) + .leftJoin( + customerProducts, + eq(customerEntitlements.customer_product_id, customerProducts.id), + ) + .where( + and( + eq(customerEntitlements.internal_customer_id, fullCus.internal_id), + isNull(entitlements.internal_product_id), + ), + ); - const formattedBalances = rawBalances.map((row) => ({ - ...row.customer_entitlement, - entitlement: { - ...row.entitlement, - feature: row.feature, - }, - customer_product: row.customer_product - ? { - ...row.customer_product, - product: null, - customer_entitlements: [], - customer_prices: [], - free_trial: null, - } - : null, - })); + const formattedBalances = rawBalances.map((row) => ({ + ...row.customer_entitlement, + entitlement: { + ...row.entitlement, + feature: row.feature, + }, + customer_product: row.customer_product + ? { + ...row.customer_product, + product: null, + customer_entitlements: [], + customer_prices: [], + free_trial: null, + } + : null, + })); - return c.json({ balances: formattedBalances }); - }, + return c.json({ balances: formattedBalances }); + }, }); diff --git a/server/src/internal/balances/utils/deduction/logDeductionUpdates.ts b/server/src/internal/balances/utils/deduction/logDeductionUpdates.ts index 56f947193..dc1f0360d 100644 --- a/server/src/internal/balances/utils/deduction/logDeductionUpdates.ts +++ b/server/src/internal/balances/utils/deduction/logDeductionUpdates.ts @@ -1,7 +1,7 @@ import { - fullCustomerToCustomerEntitlements, type FullCustomer, findCustomerEntitlementById, + fullCustomerToCustomerEntitlements, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import type { DeductionUpdate } from "../types/deductionUpdate.js"; diff --git a/server/src/internal/balances/utils/deduction/prepareFeatureDeduction.ts b/server/src/internal/balances/utils/deduction/prepareFeatureDeduction.ts index fc08ac688..3e0a47d14 100644 --- a/server/src/internal/balances/utils/deduction/prepareFeatureDeduction.ts +++ b/server/src/internal/balances/utils/deduction/prepareFeatureDeduction.ts @@ -1,7 +1,7 @@ import { cusEntToStartingBalance, - fullCustomerToCustomerEntitlements, type FullCustomer, + fullCustomerToCustomerEntitlements, getMaxOverage, getRelevantFeatures, isAllocatedCustomerEntitlement, @@ -43,9 +43,9 @@ export const prepareFeatureDeduction = ({ const relevantFeatures = notNullish(targetBalance) ? [feature] : getRelevantFeatures({ - features: ctx.features, - featureId: feature.id, - }); + features: ctx.features, + featureId: feature.id, + }); // Get customer entitlements for these features (includes both product and loose entitlements) const cusEnts = fullCustomerToCustomerEntitlements({ diff --git a/server/src/internal/customers/CusReadService.ts b/server/src/internal/customers/CusReadService.ts index 5c41f93e0..df1200d99 100644 --- a/server/src/internal/customers/CusReadService.ts +++ b/server/src/internal/customers/CusReadService.ts @@ -1,6 +1,6 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { Customer, customers } from "@autumn/shared"; +import { type Customer, customers } from "@autumn/shared"; import { inArray } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; export class CusReadService { static async getInInternalIds({ diff --git a/server/src/internal/customers/CusSearchService.ts b/server/src/internal/customers/CusSearchService.ts index 972531e57..d335ec917 100644 --- a/server/src/internal/customers/CusSearchService.ts +++ b/server/src/internal/customers/CusSearchService.ts @@ -527,7 +527,10 @@ export class CusSearchService { } // Call searchByProduct if we have version filters OR status filters - if ((filters?.version && filters?.version.length > 0) || (filters?.status && filters?.status.length > 0)) { + if ( + (filters?.version && filters?.version.length > 0) || + (filters?.status && filters?.status.length > 0) + ) { return await CusSearchService.searchByProduct({ db, orgId, diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts index 81156f0ed..d12bcee72 100644 --- a/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts +++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts @@ -1,14 +1,14 @@ -import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { ItemSet } from "@/utils/models/ItemSet.js"; import { - AttachConfig, - FullCusProduct, + type AttachConfig, + type FullCusProduct, getCusProductMinQuantity, - ProductOptions, + type ProductOptions, } from "@autumn/shared"; import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; +import type { ItemSet } from "@/utils/models/ItemSet.js"; import { mergeItemSets } from "./mergeItemSets.js"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; export const getProdListWithoutEntities = ({ attachParams, @@ -19,7 +19,7 @@ export const getProdListWithoutEntities = ({ }) => { const newProdList = structuredClone(productsList); for (let i = 0; i < newProdList.length; i++) { - let productOptions = newProdList[i]; + const productOptions = newProdList[i]; newProdList[i] = { ...productOptions, quantity: diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts index 0368af672..2dde9c22a 100644 --- a/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts +++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts @@ -1,4 +1,4 @@ -import { ItemSet } from "@/utils/models/ItemSet.js"; +import type { ItemSet } from "@/utils/models/ItemSet.js"; export const mergeItemSets = ({ curItemSet, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts index 235d8beb3..4cde37e5a 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts @@ -1,5 +1,5 @@ +import type Stripe from "stripe"; import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; -import Stripe from "stripe"; export const getNextCycle = (stripeSubs: Stripe.Subscription[]) => { const { end } = subToPeriodStartEnd({ sub: stripeSubs[0] }); diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts index a53b7770e..5a0a8cc62 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts @@ -1,4 +1,4 @@ -import Stripe from "stripe"; +import type Stripe from "stripe"; export const shouldCancelSub = ({ sub, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts b/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts index 0cca8d14d..9c08da624 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts @@ -1,5 +1,4 @@ -import { EntitlementWithFeature } from "@autumn/shared"; -import { Price } from "@autumn/shared"; +import type { EntitlementWithFeature, Price } from "@autumn/shared"; export const hasPriceIdsChanged = ({ oldPrices, diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts index 432d4a43c..84c50c22c 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts @@ -1,19 +1,18 @@ -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js"; -import { Proration } from "@/internal/invoices/prorationUtils.js"; -import { getUsageFromBalance } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.js"; -import { generateId } from "@/utils/genUtils.js"; import { - FullEntitlement, - FullCustomerEntitlement, - PreviewLineItem, - Price, + AttachReplaceableSchema, + type FullCustomerEntitlement, + type FullEntitlement, + type PreviewLineItem, + type Price, usageToFeatureName, } from "@autumn/shared"; - -import { attachParamsToProduct } from "../convertAttachParams.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js"; +import type { Proration } from "@/internal/invoices/prorationUtils.js"; +import { getUsageFromBalance } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.js"; import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceToInvoiceItem.js"; -import { AttachReplaceableSchema } from "@autumn/shared"; +import { generateId } from "@/utils/genUtils.js"; +import { attachParamsToProduct } from "../convertAttachParams.js"; export const getContUseDowngradeItems = async ({ price, @@ -34,31 +33,31 @@ export const getContUseDowngradeItems = async ({ proration?: Proration; logger: any; }) => { - let prevInvoiceItem = curItem; - let prevBalance = prevCusEnt.entitlement.allowance! - curUsage; + const prevInvoiceItem = curItem; + const prevBalance = prevCusEnt.entitlement.allowance! - curUsage; const product = attachParamsToProduct({ attachParams }); const feature = prevCusEnt.entitlement.feature; - let { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({ + const { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({ ent: prevCusEnt.entitlement, price, balance: prevBalance, }); - let { usage: newUsage, overage: newOverage } = getUsageFromBalance({ + const { usage: newUsage, overage: newOverage } = getUsageFromBalance({ ent, price, balance: prevBalance, }); if (prevOverage == 0) { - let { usage: newUsage } = getUsageFromBalance({ + const { usage: newUsage } = getUsageFromBalance({ ent, price, balance: ent.allowance! - curUsage, }); - let newItem = priceToInvoiceItem({ + const newItem = priceToInvoiceItem({ price, ent, org: attachParams.org, @@ -87,9 +86,9 @@ export const getContUseDowngradeItems = async ({ now: attachParams.now, }); - let numReplaceables = newUsage - prevUsage; + const numReplaceables = newUsage - prevUsage; - let replaceables = Array.from({ length: numReplaceables }, (_, i) => + const replaceables = Array.from({ length: numReplaceables }, (_, i) => AttachReplaceableSchema.parse({ ent: ent, id: generateId("rep"), @@ -103,7 +102,7 @@ export const getContUseDowngradeItems = async ({ feature, }); - let replaceableItem = constructPreviewItem({ + const replaceableItem = constructPreviewItem({ priceStr: `${numReplaceables} free ${featureName}`, price, description: `${product.name} - ${featureName}`, diff --git a/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts b/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts index 16ba2567f..c54fbf3bf 100644 --- a/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts +++ b/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts @@ -1,7 +1,7 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { Entitlement, Price } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; -import { Entitlement, Price } from "@autumn/shared"; export const insertCustomItems = async ({ db, diff --git a/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts b/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts index 6d53bddd7..b5602b59b 100644 --- a/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts +++ b/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts @@ -1,4 +1,4 @@ -import Stripe from "stripe"; +import type Stripe from "stripe"; // Normalize items for comparison (order-agnostic, price id as string) const normalizePhaseItems = ( diff --git a/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts b/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts index 680c55522..12c79cf9c 100644 --- a/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts +++ b/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts @@ -1,5 +1,5 @@ import { differenceInDays } from "date-fns"; -import Stripe from "stripe"; +import type Stripe from "stripe"; export const phaseAndUnixMatch = ({ phase, diff --git a/server/src/internal/customers/attach/models/AttachFlags.ts b/server/src/internal/customers/attach/models/AttachFlags.ts index be175d18d..64aa83604 100644 --- a/server/src/internal/customers/attach/models/AttachFlags.ts +++ b/server/src/internal/customers/attach/models/AttachFlags.ts @@ -1,4 +1,4 @@ -import { AttachBranch } from "@autumn/shared"; +import type { AttachBranch } from "@autumn/shared"; export enum ProrationBehavior { Immediately = "immediately", diff --git a/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts b/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts index f04202d95..0e9842129 100644 --- a/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts +++ b/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts @@ -1,5 +1,5 @@ -import { AttachConfig, CusProductStatus } from "@autumn/shared"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; +import { type AttachConfig, CusProductStatus } from "@autumn/shared"; +import type { AttachParams } from "../../cusProducts/AttachParams.js"; export const handleUnifiedAttach = async ({ req, diff --git a/server/src/internal/customers/cusProducts/cusEnts/RepService.ts b/server/src/internal/customers/cusProducts/cusEnts/RepService.ts index 110378618..8c002acd6 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/RepService.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/RepService.ts @@ -1,6 +1,10 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { InsertReplaceable, Replaceable, replaceables } from "@autumn/shared"; +import { + type InsertReplaceable, + type Replaceable, + replaceables, +} from "@autumn/shared"; import { eq, inArray } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; export class RepService { static async insert({ diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts index 0b60720c4..2163faaee 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts @@ -1,6 +1,6 @@ // 1. Get min next reset at cus ent -import { Feature, FullCustomerEntitlement } from "@autumn/shared"; +import type { Feature, FullCustomerEntitlement } from "@autumn/shared"; export const getMinNextResetAtCusEnt = ({ cusEnts, diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts index 74c4ba4dc..22023e9c8 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts @@ -1,9 +1,9 @@ -import { - type CustomerEntitlement, - type EntitlementWithFeature, - type FullCusProduct, - type FullCustomerEntitlement, - type Rollover, +import type { + CustomerEntitlement, + EntitlementWithFeature, + FullCusProduct, + FullCustomerEntitlement, + Rollover, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { generateId } from "@/utils/genUtils.js"; diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts index 86c322cda..e064b81a7 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCustomer.ts @@ -52,9 +52,11 @@ export const setCachedApiCustomer = async ({ }); // Build entity api customers (entity-level features only) - const filteredFullCus = filterEntityLevelCustomerEntitlementsFromFullCustomer({ - fullCustomer: fullCus, - }); + const filteredFullCus = filterEntityLevelCustomerEntitlementsFromFullCustomer( + { + fullCustomer: fullCus, + }, + ); // Build entities first const entityBatch: { diff --git a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedGrantedBalance.ts b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedGrantedBalance.ts index 0de25416a..129789e54 100644 --- a/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedGrantedBalance.ts +++ b/server/src/internal/customers/cusUtils/apiCusCacheUtils/setCachedGrantedBalance.ts @@ -75,9 +75,10 @@ export const setCachedGrantedBalance = async ({ // ============================================================================ // 2. Build entity balances batch (entity-level products only) // ============================================================================ - const entityLevelCusProducts = filterEntityLevelCustomerEntitlementsFromFullCustomer({ - fullCustomer: fullCus, - }).customer_products; + const entityLevelCusProducts = + filterEntityLevelCustomerEntitlementsFromFullCustomer({ + fullCustomer: fullCus, + }).customer_products; const entityBatch: EntityBatchItem[] = []; diff --git a/server/src/internal/customers/getFullCusQuery.ts b/server/src/internal/customers/getFullCusQuery.ts index 5450e120a..89d7983f2 100644 --- a/server/src/internal/customers/getFullCusQuery.ts +++ b/server/src/internal/customers/getFullCusQuery.ts @@ -1,21 +1,21 @@ import type { - AppEnv, - CusProductStatus, - ListCustomersV2Params, + AppEnv, + CusProductStatus, + ListCustomersV2Params, } from "@autumn/shared"; import { type SQL, sql } from "drizzle-orm"; const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => { - const withStatusFilter = () => { - return inStatuses - ? sql`AND cp.status = ANY(ARRAY[${sql.join( - inStatuses.map((status) => sql`${status}`), - sql`, `, - )}])` - : sql``; - }; + const withStatusFilter = () => { + return inStatuses + ? sql`AND cp.status = ANY(ARRAY[${sql.join( + inStatuses.map((status) => sql`${status}`), + sql`, `, + )}])` + : sql``; + }; - return sql` + return sql` customer_products_with_prices AS ( SELECT cp.*, @@ -83,11 +83,11 @@ const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => { }; const buildEntitiesCTE = (withEntities: boolean) => { - if (!withEntities) { - return sql``; - } + if (!withEntities) { + return sql``; + } - return sql` + return sql` customer_entities AS ( SELECT COALESCE( @@ -105,11 +105,11 @@ const buildEntitiesCTE = (withEntities: boolean) => { }; const buildEntityCTE = (entityId?: string) => { - if (!entityId) { - return sql``; - } + if (!entityId) { + return sql``; + } - return sql` + return sql` entity_record AS ( SELECT * FROM entities e WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record) @@ -122,15 +122,15 @@ const buildEntityCTE = (entityId?: string) => { }; const buildTrialsUsedCTE = ( - withTrialsUsed: boolean, - orgId: string, - env: AppEnv, + withTrialsUsed: boolean, + orgId: string, + env: AppEnv, ) => { - if (!withTrialsUsed) { - return sql``; - } + if (!withTrialsUsed) { + return sql``; + } - return sql` + return sql` customer_trials_used AS ( SELECT COALESCE( @@ -153,14 +153,14 @@ const buildTrialsUsedCTE = ( }; const buildSubscriptionsCTE = ( - withSubs: boolean, - inStatuses?: CusProductStatus[], + withSubs: boolean, + inStatuses?: CusProductStatus[], ) => { - if (!withSubs) { - return sql``; - } + if (!withSubs) { + return sql``; + } - return sql` + return sql` customer_subscriptions AS ( SELECT COALESCE( @@ -177,7 +177,7 @@ const buildSubscriptionsCTE = ( }; const buildExtraEntitlementsCTE = () => { - return sql` + return sql` extra_customer_entitlements AS ( SELECT COALESCE( @@ -222,15 +222,15 @@ const buildExtraEntitlementsCTE = () => { }; const buildInvoicesCTE = (hasEntityCTE: boolean) => { - const entityFilter = hasEntityCTE - ? sql`AND ( + const entityFilter = hasEntityCTE + ? sql`AND ( NOT EXISTS (SELECT 1 FROM entity_record) OR i.internal_entity_id = (SELECT internal_id FROM entity_record LIMIT 1) OR i.internal_entity_id IS NULL )` - : sql``; + : sql``; - return sql` + return sql` customer_invoices AS ( SELECT COALESCE( @@ -246,21 +246,21 @@ const buildInvoicesCTE = (hasEntityCTE: boolean) => { }; export const getFullCusQuery = ( - idOrInternalId: string, - orgId: string, - env: AppEnv, - inStatuses: CusProductStatus[], - includeInvoices: boolean, - withEntities: boolean, - withTrialsUsed: boolean, - withSubs: boolean, - withEvents: boolean, - entityId?: string, + idOrInternalId: string, + orgId: string, + env: AppEnv, + inStatuses: CusProductStatus[], + includeInvoices: boolean, + withEntities: boolean, + withTrialsUsed: boolean, + withSubs: boolean, + withEvents: boolean, + entityId?: string, ) => { - const sqlChunks: SQL[] = []; + const sqlChunks: SQL[] = []; - // Step 1: Get customer record - sqlChunks.push(sql` + // Step 1: Get customer record + sqlChunks.push(sql` WITH customer_record AS ( SELECT * FROM customers c WHERE ( @@ -273,49 +273,49 @@ export const getFullCusQuery = ( ) `); - // Step 2: Get entities - if (withEntities) { - sqlChunks.push(sql`, `); - sqlChunks.push(buildEntitiesCTE(withEntities)); - } + // Step 2: Get entities + if (withEntities) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildEntitiesCTE(withEntities)); + } - // Step 3: Get entity - if (entityId) { - sqlChunks.push(sql`, `); - sqlChunks.push(buildEntityCTE(entityId)); - } + // Step 3: Get entity + if (entityId) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildEntityCTE(entityId)); + } - // Add customer products CTE - sqlChunks.push(sql`, `); - // sqlChunks.push(buildCusProductsCTE(inStatuses)); - sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses)); + // Add customer products CTE + sqlChunks.push(sql`, `); + // sqlChunks.push(buildCusProductsCTE(inStatuses)); + sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses)); - // Conditionally add trials used CTE - if (withTrialsUsed) { - sqlChunks.push(sql`, `); - sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env)); - } + // Conditionally add trials used CTE + if (withTrialsUsed) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env)); + } - // Conditionally add subscriptions CTE - if (withSubs) { - sqlChunks.push(sql`, `); - sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses)); - } + // Conditionally add subscriptions CTE + if (withSubs) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses)); + } - // Unconditionally add extra entitlements CTE - sqlChunks.push(sql`, `); - sqlChunks.push(buildExtraEntitlementsCTE()); + // Unconditionally add extra entitlements CTE + sqlChunks.push(sql`, `); + sqlChunks.push(buildExtraEntitlementsCTE()); - // Conditionally add invoices CTE - if (includeInvoices) { - sqlChunks.push(sql`, `); - sqlChunks.push(buildInvoicesCTE(!!entityId)); - } + // Conditionally add invoices CTE + if (includeInvoices) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildInvoicesCTE(!!entityId)); + } - // Conditionally add events CTE - if (withEvents) { - sqlChunks.push(sql`, `); - sqlChunks.push(sql` + // Conditionally add events CTE + if (withEvents) { + sqlChunks.push(sql`, `); + sqlChunks.push(sql` customer_events AS ( SELECT COALESCE( @@ -336,11 +336,11 @@ export const getFullCusQuery = ( AND e.set_usage = false ) `); - } + } - // Build final SELECT - const selectFieldsChunks: SQL[] = []; - selectFieldsChunks.push(sql` + // Build final SELECT + const selectFieldsChunks: SQL[] = []; + selectFieldsChunks.push(sql` cr.*, COALESCE( (SELECT json_agg(cpwp) FROM customer_products_with_prices cpwp), @@ -348,131 +348,131 @@ export const getFullCusQuery = ( ) AS customer_products `); - // Add entities to SELECT if withEntities is true - if (withEntities) { - selectFieldsChunks.push(sql`, + // Add entities to SELECT if withEntities is true + if (withEntities) { + selectFieldsChunks.push(sql`, (SELECT entities FROM customer_entities) AS entities`); - } + } - // Add entity to SELECT if entityId is provided - if (entityId) { - selectFieldsChunks.push(sql`, + // Add entity to SELECT if entityId is provided + if (entityId) { + selectFieldsChunks.push(sql`, (SELECT row_to_json(er) FROM entity_record er LIMIT 1) AS entity`); - } + } - // Add trials used to SELECT if withTrialsUsed is true - if (withTrialsUsed) { - selectFieldsChunks.push(sql`, + // Add trials used to SELECT if withTrialsUsed is true + if (withTrialsUsed) { + selectFieldsChunks.push(sql`, (SELECT trials_used FROM customer_trials_used) AS trials_used`); - } + } - // Add subscriptions to SELECT if withSubs is true - if (withSubs) { - selectFieldsChunks.push(sql`, + // Add subscriptions to SELECT if withSubs is true + if (withSubs) { + selectFieldsChunks.push(sql`, (SELECT subscriptions FROM customer_subscriptions) AS subscriptions`); - } + } - selectFieldsChunks.push(sql`, + selectFieldsChunks.push(sql`, (SELECT extra_customer_entitlements FROM extra_customer_entitlements) AS extra_customer_entitlements`); - if (includeInvoices) { - selectFieldsChunks.push(sql`, + if (includeInvoices) { + selectFieldsChunks.push(sql`, (SELECT invoices FROM customer_invoices) AS invoices`); - } + } - if (withEvents) { - selectFieldsChunks.push(sql`, + if (withEvents) { + selectFieldsChunks.push(sql`, (SELECT events FROM customer_events) AS events`); - } + } - sqlChunks.push(sql` + sqlChunks.push(sql` SELECT ${sql.join(selectFieldsChunks, sql``)} FROM customer_record cr `); - return sql.join(sqlChunks, sql``); + return sql.join(sqlChunks, sql``); }; export const getPaginatedFullCusQuery = ({ - orgId, - env, - inStatuses, - includeInvoices, - withEntities, - withTrialsUsed, - withSubs, - limit = 10, - offset = 0, - withEvents = false, - entityId, - internalCustomerIds, - plans, - search, + orgId, + env, + inStatuses, + includeInvoices, + withEntities, + withTrialsUsed, + withSubs, + limit = 10, + offset = 0, + withEvents = false, + entityId, + internalCustomerIds, + plans, + search, }: { - orgId: string; - env: AppEnv; - inStatuses?: CusProductStatus[]; - includeInvoices: boolean; - withEntities: boolean; - withTrialsUsed: boolean; - withSubs: boolean; - limit: number; - offset: number; - withEvents?: boolean; - entityId?: string; - internalCustomerIds?: string[]; - plans?: ListCustomersV2Params["plans"]; - search?: string; + orgId: string; + env: AppEnv; + inStatuses?: CusProductStatus[]; + includeInvoices: boolean; + withEntities: boolean; + withTrialsUsed: boolean; + withSubs: boolean; + limit: number; + offset: number; + withEvents?: boolean; + entityId?: string; + internalCustomerIds?: string[]; + plans?: ListCustomersV2Params["plans"]; + search?: string; }) => { - const withStatusFilter = () => { - return inStatuses?.length - ? sql`AND cp.status = ANY(ARRAY[${sql.join( - inStatuses.map((status) => sql`${status}`), - sql`, `, - )}])` - : sql``; - }; + const withStatusFilter = () => { + return inStatuses?.length + ? sql`AND cp.status = ANY(ARRAY[${sql.join( + inStatuses.map((status) => sql`${status}`), + sql`, `, + )}])` + : sql``; + }; - const withCustomerProductFilter = () => { - const hasPlansFilter = plans && plans.length > 0; + const withCustomerProductFilter = () => { + const hasPlansFilter = plans && plans.length > 0; - // Only filter customers by plans, not by status - // Status filtering is applied to customer_products, not to exclude customers - // This allows customers with only loose entitlements (no customer_products) to be included - if (!hasPlansFilter) return sql``; + // Only filter customers by plans, not by status + // Status filtering is applied to customer_products, not to exclude customers + // This allows customers with only loose entitlements (no customer_products) to be included + if (!hasPlansFilter) return sql``; - const planConditions = plans.map((plan) => { - if (plan.versions && plan.versions.length > 0) { - return sql`(p_filter.id = ${plan.id} AND p_filter.version IN (${sql.join( - plan.versions.map((v) => sql`${v}`), - sql`, `, - )}))`; - } - return sql`p_filter.id = ${plan.id}`; - }); + const planConditions = plans.map((plan) => { + if (plan.versions && plan.versions.length > 0) { + return sql`(p_filter.id = ${plan.id} AND p_filter.version IN (${sql.join( + plan.versions.map((v) => sql`${v}`), + sql`, `, + )}))`; + } + return sql`p_filter.id = ${plan.id}`; + }); - return sql`AND EXISTS ( + return sql`AND EXISTS ( SELECT 1 FROM customer_products cp_filter JOIN products p_filter ON cp_filter.internal_product_id = p_filter.internal_id WHERE cp_filter.internal_customer_id = c.internal_id AND (${sql.join(planConditions, sql` OR `)}) )`; - }; + }; - const withSearchFilter = () => { - if (!search) return sql``; - const pattern = `%${search}%`; - return sql`AND ( + const withSearchFilter = () => { + if (!search) return sql``; + const pattern = `%${search}%`; + return sql`AND ( c.id ILIKE ${pattern} OR c.name ILIKE ${pattern} OR c.email ILIKE ${pattern} )`; - }; + }; - // ADDITION: Unconditionally add extra entitlements CTE (305-308) - // This matches the style of the rest of the CTE construction blocks. - // Extra entitlements are those without a customer_product_id (loose entitlements) - const extraEntitlementsCTE = sql`, extra_customer_entitlements AS ( + // ADDITION: Unconditionally add extra entitlements CTE (305-308) + // This matches the style of the rest of the CTE construction blocks. + // Extra entitlements are those without a customer_product_id (loose entitlements) + const extraEntitlementsCTE = sql`, extra_customer_entitlements AS ( SELECT ce.internal_customer_id, COALESCE( @@ -516,19 +516,20 @@ export const getPaginatedFullCusQuery = ({ GROUP BY ce.internal_customer_id )`; - return sql` + return sql` WITH customer_records AS ( SELECT c.* FROM customers c WHERE c.org_id = ${orgId} AND c.env = ${env} - ${internalCustomerIds && internalCustomerIds.length > 0 - ? sql`AND c.internal_id IN (${sql.join( - internalCustomerIds.map((id) => sql`${id}`), - sql`, `, - )})` - : sql`` - } + ${ + internalCustomerIds && internalCustomerIds.length > 0 + ? sql`AND c.internal_id IN (${sql.join( + internalCustomerIds.map((id) => sql`${id}`), + sql`, `, + )})` + : sql`` + } ${withCustomerProductFilter()} ${withSearchFilter()} ORDER BY c.created_at DESC @@ -607,8 +608,9 @@ export const getPaginatedFullCusQuery = ({ GROUP BY cpwp.internal_customer_id ) - ${withSubs - ? sql`, customer_subscriptions AS ( + ${ + withSubs + ? sql`, customer_subscriptions AS ( SELECT cpwp.internal_customer_id, COALESCE( @@ -619,11 +621,12 @@ export const getPaginatedFullCusQuery = ({ JOIN subscriptions s ON s.stripe_id = ANY(cpwp.subscription_ids) GROUP BY cpwp.internal_customer_id )` - : sql`` - } + : sql`` + } - ${withEntities - ? sql`, customer_entities AS ( + ${ + withEntities + ? sql`, customer_entities AS ( SELECT e.internal_customer_id, COALESCE( @@ -634,11 +637,12 @@ export const getPaginatedFullCusQuery = ({ WHERE e.internal_customer_id IN (SELECT internal_id FROM customer_records) GROUP BY e.internal_customer_id )` - : sql`` - } + : sql`` + } - ${includeInvoices - ? sql`, customer_invoices AS ( + ${ + includeInvoices + ? sql`, customer_invoices AS ( SELECT i.internal_customer_id, COALESCE( @@ -649,11 +653,12 @@ export const getPaginatedFullCusQuery = ({ WHERE i.internal_customer_id IN (SELECT internal_id FROM customer_records) GROUP BY i.internal_customer_id )` - : sql`` - } + : sql`` + } - ${withTrialsUsed - ? sql`, customer_trials_used AS ( + ${ + withTrialsUsed + ? sql`, customer_trials_used AS ( SELECT cp.internal_customer_id, json_agg(json_build_object( @@ -668,8 +673,8 @@ export const getPaginatedFullCusQuery = ({ AND cp.free_trial_id IS NOT NULL GROUP BY cp.internal_customer_id )` - : sql`` - } + : sql`` + } ${extraEntitlementsCTE} diff --git a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts index f7640211b..03d01dacc 100644 --- a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts +++ b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts @@ -90,7 +90,9 @@ export const handleDecreaseAndTransfer = async ({ replaceables: [], entities: fullCus.entities, features, - internalEntityId: toEntity?.internal_id ? toEntity.internal_id : undefined, + internalEntityId: toEntity?.internal_id + ? toEntity.internal_id + : undefined, entityId: toEntity?.id ? toEntity.id : undefined, }, product, diff --git a/server/src/internal/customers/previews/checkProductUtils.ts b/server/src/internal/customers/previews/checkProductUtils.ts index a75958714..82b6fc452 100644 --- a/server/src/internal/customers/previews/checkProductUtils.ts +++ b/server/src/internal/customers/previews/checkProductUtils.ts @@ -1,17 +1,21 @@ +import type { + Feature, + Organization, + ProductItem, + ProductV2, +} from "@autumn/shared"; +import { getFeatureNameWithCapital } from "@/internal/features/utils/displayUtils.js"; import { featurePricetoPricecnItem, getPriceText, } from "@/internal/products/pricecn/pricecnUtils.js"; import { isFeatureItem, + isFeaturePriceItem, isPriceItem, } from "@/internal/products/product-items/productItemUtils/getItemType.js"; - -import { Feature, Organization, ProductItem, ProductV2 } from "@autumn/shared"; -import { formatCurrency, formatTiers } from "./previewUtils.js"; -import { isFeaturePriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js"; import { notNullish } from "@/utils/genUtils.js"; -import { getFeatureNameWithCapital } from "@/internal/features/utils/displayUtils.js"; +import { formatCurrency, formatTiers } from "./previewUtils.js"; export const getProductChargeText = ({ product, @@ -22,10 +26,10 @@ export const getProductChargeText = ({ org: Organization; features: Feature[]; }) => { - let basePrices = product.items.filter((i) => isPriceItem(i)); - let total = basePrices.reduce((acc, curr) => acc + curr.price!, 0); + const basePrices = product.items.filter((i) => isPriceItem(i)); + const total = basePrices.reduce((acc, curr) => acc + curr.price!, 0); - let itemStrs = []; + const itemStrs = []; if (total > 0) { itemStrs.push( formatCurrency({ @@ -35,18 +39,18 @@ export const getProductChargeText = ({ ); } - let prepaidPrices = product.items.filter( + const prepaidPrices = product.items.filter( (i) => isFeaturePriceItem(i) && i.usage_model == "prepaid", ); - let prepaidStrings = prepaidPrices.map((i) => { - let feature = features.find((f) => f.id === i.feature_id); - let priceStr = formatTiers({ + const prepaidStrings = prepaidPrices.map((i) => { + const feature = features.find((f) => f.id === i.feature_id); + const priceStr = formatTiers({ tiers: i.tiers!, org, }); - let featureStr = + const featureStr = i.billing_units && i.billing_units > 1 ? `${i.billing_units} ${feature?.name}` : feature?.name; @@ -67,15 +71,15 @@ export const getItemDescription = ({ product: ProductV2; org: Organization; }) => { - let prices = product.items.filter((i) => !isFeatureItem(i)); + const prices = product.items.filter((i) => !isFeatureItem(i)); - let priceStr = getPriceText({ + const priceStr = getPriceText({ item, org, }); if (isPriceItem(item)) { - let baseName = + const baseName = prices.length == 1 ? product.name : notNullish(item.interval) @@ -84,7 +88,7 @@ export const getItemDescription = ({ return baseName; } else { - let feature = features.find((f) => f.id === item.feature_id); + const feature = features.find((f) => f.id === item.feature_id); // let pricecnItem = featurePricetoPricecnItem({ // feature, // item, diff --git a/server/src/internal/customers/previews/previewUtils.ts b/server/src/internal/customers/previews/previewUtils.ts index ccdd7a278..84a14014a 100644 --- a/server/src/internal/customers/previews/previewUtils.ts +++ b/server/src/internal/customers/previews/previewUtils.ts @@ -1,4 +1,4 @@ -import { Organization, PriceTier } from "@autumn/shared"; +import type { Organization, PriceTier } from "@autumn/shared"; export const formatCurrency = ({ amount, @@ -30,11 +30,11 @@ export const formatTiers = ({ }); } - let tiersStart = formatCurrency({ + const tiersStart = formatCurrency({ amount: tiers[0].amount, defaultCurrency: org.default_currency!, }); - let tiersEnd = formatCurrency({ + const tiersEnd = formatCurrency({ amount: tiers[tiers.length - 1].amount, defaultCurrency: org.default_currency!, }); @@ -50,8 +50,8 @@ export const getItemsHtml = ({ org: Organization; }) => { let html = ""; - let pricedItems = items.filter((item) => item.amount != 0); - let totalAmount = pricedItems.reduce((acc: number, item: any) => { + const pricedItems = items.filter((item) => item.amount != 0); + const totalAmount = pricedItems.reduce((acc: number, item: any) => { return acc + item.amount; }, 0); @@ -74,7 +74,7 @@ export const getItemsHtml = ({ export const itemsToHtml = ({ items }: { items: any[] }) => { let html = ""; - for (let item of items) { + for (const item of items) { if (item.amount == 0) { continue; } diff --git a/server/src/internal/emails/sendMigrationEmail.ts b/server/src/internal/emails/sendMigrationEmail.ts index 088d358b7..e8a68a695 100644 --- a/server/src/internal/emails/sendMigrationEmail.ts +++ b/server/src/internal/emails/sendMigrationEmail.ts @@ -1,8 +1,8 @@ -import { MigrationService } from "../migrations/MigrationService.js"; +import { MigrationJobStep, type Organization } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { sendTextEmail } from "@/external/resend/resendUtils.js"; -import { MigrationJobStep, Organization } from "@autumn/shared"; -import { DrizzleCli } from "@/db/initDrizzle.js"; import { safeResend } from "@/external/resend/safeResend.js"; +import { MigrationService } from "../migrations/MigrationService.js"; import { FROM_AUTUMN } from "./constants.js"; export const sendMigrationEmail = safeResend({ @@ -15,15 +15,15 @@ export const sendMigrationEmail = safeResend({ migrationJobId: string; org: Organization; }) => { - let migrationJob = await MigrationService.getJob({ + const migrationJob = await MigrationService.getJob({ db, id: migrationJobId, }); // Send email - let getCustomersStep = + const getCustomersStep = migrationJob.step_details[MigrationJobStep.GetCustomers]; - let migrateStep = + const migrateStep = migrationJob.step_details[MigrationJobStep.MigrateCustomers]; console.log("Sending migration email"); diff --git a/server/src/internal/emails/sendOTPEmail.ts b/server/src/internal/emails/sendOTPEmail.ts index 2a08c8d9f..7183eee77 100644 --- a/server/src/internal/emails/sendOTPEmail.ts +++ b/server/src/internal/emails/sendOTPEmail.ts @@ -1,6 +1,6 @@ +import OTPEmail from "@emails/OTPEmail.js"; import { logger } from "@/external/logtail/logtailUtils.js"; import { createResendCli } from "@/external/resend/resendUtils.js"; -import OTPEmail from "@emails/OTPEmail.js"; import { FROM_AUTUMN } from "./constants.js"; const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => { diff --git a/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts b/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts index 7939ab470..cc976cb5b 100644 --- a/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts +++ b/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts @@ -1,7 +1,7 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { EntityService } from "@/internal/api/entities/EntityService.js"; -import { Feature, getFeatureName } from "@autumn/shared"; +import { type Feature, getFeatureName } from "@autumn/shared"; import { AppEnv, Entity } from "autumn-js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { EntityService } from "@/internal/api/entities/EntityService.js"; export const getEntityInvoiceDescription = async ({ db, @@ -15,12 +15,12 @@ export const getEntityInvoiceDescription = async ({ logger: any; }) => { try { - let entity = await EntityService.getByInternalId({ + const entity = await EntityService.getByInternalId({ db, internalId: internalEntityId, }); - let feature = features.find( + const feature = features.find( (f) => f.internal_id == entity?.internal_feature_id, ); @@ -32,7 +32,7 @@ export const getEntityInvoiceDescription = async ({ } if (feature && entDetails) { - let featureName = getFeatureName({ + const featureName = getFeatureName({ feature, plural: false, capitalize: true, diff --git a/server/src/internal/entities/entityUtils/entityUtils.ts b/server/src/internal/entities/entityUtils/entityUtils.ts index 5d044e326..88eed19d0 100644 --- a/server/src/internal/entities/entityUtils/entityUtils.ts +++ b/server/src/internal/entities/entityUtils/entityUtils.ts @@ -1,10 +1,10 @@ -import { generateId, notNullish, nullish } from "@/utils/genUtils.js"; import { - AppEnv, - Entity, + type AppEnv, + type Entity, FeatureType, FullCusEntWithFullCusProduct, } from "@autumn/shared"; +import { generateId, notNullish, nullish } from "@/utils/genUtils.js"; export const constructEntity = ({ inputEntity, @@ -21,7 +21,7 @@ export const constructEntity = ({ env: AppEnv; deleted?: boolean; }) => { - let entity: Entity = { + const entity: Entity = { internal_id: generateId("ety"), id: inputEntity.id, name: inputEntity.name, diff --git a/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts b/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts index ba74b244f..1449db1f1 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts @@ -1,6 +1,5 @@ -import { notNullish } from "@/utils/genUtils.js"; -import { nullish } from "@/utils/genUtils.js"; -import { Entity } from "@autumn/shared"; +import type { Entity } from "@autumn/shared"; +import { notNullish, nullish } from "@/utils/genUtils.js"; export const logEntityToAction = ({ entityToAction, @@ -35,11 +34,13 @@ export const getEntityToAction = ({ feature: any; logger: any; }) => { - let entityToAction: any = {}; + const entityToAction: any = {}; let createCount = 0; - let replacedEntities: string[] = []; + const replacedEntities: string[] = []; for (const inputEntity of inputEntities) { - let curEntity = existingEntities.find((e: any) => e.id === inputEntity.id); + const curEntity = existingEntities.find( + (e: any) => e.id === inputEntity.id, + ); if (curEntity && curEntity.deleted) { entityToAction[inputEntity.id] = { diff --git a/server/src/internal/features/utils/displayUtils.ts b/server/src/internal/features/utils/displayUtils.ts index 08aace632..e15c4b2b9 100644 --- a/server/src/internal/features/utils/displayUtils.ts +++ b/server/src/internal/features/utils/displayUtils.ts @@ -1,6 +1,4 @@ -import { Feature } from "@autumn/shared"; - -import { Organization } from "@autumn/shared"; +import { type Feature, Organization } from "@autumn/shared"; export const getFeatureName = ({ feature, diff --git a/server/src/internal/invoices/handlers/handleGetStripeInvoice.ts b/server/src/internal/invoices/handlers/handleGetStripeInvoice.ts index 18fdb2eb8..d9a45a6c4 100644 --- a/server/src/internal/invoices/handlers/handleGetStripeInvoice.ts +++ b/server/src/internal/invoices/handlers/handleGetStripeInvoice.ts @@ -1,5 +1,5 @@ -import { createRoute } from "@/honoMiddlewares/routeHandler.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; /** * Get Stripe invoice by Stripe invoice ID diff --git a/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts b/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts index e28e61241..8788fd432 100644 --- a/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts +++ b/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts @@ -1,6 +1,11 @@ -import { Organization, Price, Product, UsagePriceConfig } from "@autumn/shared"; +import type { + Organization, + Price, + Product, + UsagePriceConfig, +} from "@autumn/shared"; import { Decimal } from "decimal.js"; -import Stripe from "stripe"; +import type Stripe from "stripe"; export const constructStripeInvoiceItem = ({ product, @@ -29,7 +34,7 @@ export const constructStripeInvoiceItem = ({ new Decimal(amount).mul(100).round().toNumber(), ); - let priceData = + const priceData = amountInCents > 0 ? { price_data: { @@ -43,7 +48,7 @@ export const constructStripeInvoiceItem = ({ currency: org.default_currency || "usd", }; - let invoiceItem: Stripe.InvoiceItemCreateParams = { + const invoiceItem: Stripe.InvoiceItemCreateParams = { subscription: stripeSubId, ...priceData, diff --git a/server/src/internal/invoices/invoiceRouter.ts b/server/src/internal/invoices/invoiceRouter.ts index 5d836c2a4..d47d7e714 100644 --- a/server/src/internal/invoices/invoiceRouter.ts +++ b/server/src/internal/invoices/invoiceRouter.ts @@ -11,7 +11,8 @@ const invoiceRedirectLimiter = rateLimiter({ windowMs: 60 * 1000, // 1 minute limit: 10, standardHeaders: "draft-6", - keyGenerator: (c) => c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown", + keyGenerator: (c) => + c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown", }); publicInvoiceRouter.get( @@ -26,4 +27,4 @@ publicInvoiceRouter.get( */ export const invoiceRouter = new Hono(); -invoiceRouter.get("/:stripe_invoice_id/stripe", ...handleGetStripeInvoice); \ No newline at end of file +invoiceRouter.get("/:stripe_invoice_id/stripe", ...handleGetStripeInvoice); diff --git a/server/src/internal/migrations/migrationUtils.ts b/server/src/internal/migrations/migrationUtils.ts index 9c77e5b28..64a8f6c5d 100644 --- a/server/src/internal/migrations/migrationUtils.ts +++ b/server/src/internal/migrations/migrationUtils.ts @@ -1,11 +1,11 @@ -import { generateId } from "@/utils/genUtils.js"; import { - AppEnv, - MigrationError, - MigrationJob, + type AppEnv, + type MigrationError, + type MigrationJob, MigrationJobStep, - Product, + type Product, } from "@autumn/shared"; +import { generateId } from "@/utils/genUtils.js"; export const constructMigrationError = ({ migrationJobId, @@ -20,7 +20,7 @@ export const constructMigrationError = ({ code: string; message: string; }) => { - let migrationError: MigrationError = { + const migrationError: MigrationError = { migration_job_id: migrationJobId, internal_customer_id: internalCustomerId, data, @@ -40,7 +40,7 @@ export const constructMigrationJob = ({ fromProduct: Product; toProduct: Product; }) => { - let migrationJob: MigrationJob = { + const migrationJob: MigrationJob = { id: generateId("mig_job"), created_at: Date.now(), updated_at: Date.now(), diff --git a/server/src/internal/misc/configs/handlers/handleNukeOrganisationConfiguration.ts b/server/src/internal/misc/configs/handlers/handleNukeOrganisationConfiguration.ts index 43147875e..81c375601 100644 --- a/server/src/internal/misc/configs/handlers/handleNukeOrganisationConfiguration.ts +++ b/server/src/internal/misc/configs/handlers/handleNukeOrganisationConfiguration.ts @@ -5,31 +5,31 @@ import { FeatureService } from "@/internal/features/FeatureService"; import { ProductService } from "@/internal/products/ProductService"; export const handleNukeOrganisationConfiguration = createRoute({ - handler: async (c) => { - const ctx = c.get("ctx"); - const { db, org, env } = ctx; + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org, env } = ctx; - if (env !== AppEnv.Sandbox) { - return c.json({ error: "Cannot clear non-sandbox orgs" }, 400); - } + if (env !== AppEnv.Sandbox) { + return c.json({ error: "Cannot clear non-sandbox orgs" }, 400); + } - await CusService.deleteByOrgId({ - db, - orgId: org.id, - env: AppEnv.Sandbox, - }); + await CusService.deleteByOrgId({ + db, + orgId: org.id, + env: AppEnv.Sandbox, + }); - await ProductService.deleteByOrgId({ - db, - orgId: org.id, - env: AppEnv.Sandbox, - }); - await FeatureService.deleteByOrgId({ - db, - orgId: org.id, - env: AppEnv.Sandbox, - }); + await ProductService.deleteByOrgId({ + db, + orgId: org.id, + env: AppEnv.Sandbox, + }); + await FeatureService.deleteByOrgId({ + db, + orgId: org.id, + env: AppEnv.Sandbox, + }); - return c.json({ message: "Organisation configuration cleared" }); - }, + return c.json({ message: "Organisation configuration cleared" }); + }, }); diff --git a/server/src/internal/misc/configs/handlers/handlePushOrganisationConfiguration.ts b/server/src/internal/misc/configs/handlers/handlePushOrganisationConfiguration.ts index 9fa597a97..5ed1dc3d6 100644 --- a/server/src/internal/misc/configs/handlers/handlePushOrganisationConfiguration.ts +++ b/server/src/internal/misc/configs/handlers/handlePushOrganisationConfiguration.ts @@ -1,9 +1,9 @@ import { - apiFeatureToDbFeature, - CreateFeatureV0ParamsSchema, - CreateFreeTrialSchema, - CreateProductItemParamsSchema, - CreateProductSchema, + apiFeatureToDbFeature, + CreateFeatureV0ParamsSchema, + CreateFreeTrialSchema, + CreateProductItemParamsSchema, + CreateProductSchema, } from "@shared/index"; import { z } from "zod/v4"; import type { DrizzleCli } from "@/db/initDrizzle"; @@ -14,84 +14,84 @@ import { createProduct } from "@/internal/products/handlers/productActions/creat import { ProductService } from "@/internal/products/ProductService"; const OrganisationConfigurationSchema = z.object({ - features: z.array(CreateFeatureV0ParamsSchema).optional().default([]), - products: z.array( - CreateProductSchema.extend({ - items: z.array(CreateProductItemParamsSchema).optional().default([]), - free_trial: CreateFreeTrialSchema.nullish().optional().default(null), - }), - ), + features: z.array(CreateFeatureV0ParamsSchema).optional().default([]), + products: z.array( + CreateProductSchema.extend({ + items: z.array(CreateProductItemParamsSchema).optional().default([]), + free_trial: CreateFreeTrialSchema.nullish().optional().default(null), + }), + ), }); export const handlePushOrganisationConfiguration = createRoute({ - body: OrganisationConfigurationSchema, - handler: async (c) => { - const body = c.req.valid("json"); - const ctx = c.get("ctx"); - const { features, db, org, env } = ctx; - const products = await ProductService.listFull({ - db, - orgId: org.id, - env, - }); + body: OrganisationConfigurationSchema, + handler: async (c) => { + const body = c.req.valid("json"); + const ctx = c.get("ctx"); + const { features, db, org, env } = ctx; + const products = await ProductService.listFull({ + db, + orgId: org.id, + env, + }); - await db.transaction(async (tx) => { - const txDb = tx as unknown as DrizzleCli; - const txCtx = { ...ctx, db: txDb }; + await db.transaction(async (tx) => { + const txDb = tx as unknown as DrizzleCli; + const txCtx = { ...ctx, db: txDb }; - for (const apiFeature of body.features) { - if (features.some((x) => x.id === apiFeature.id)) { - continue; - } + for (const apiFeature of body.features) { + if (features.some((x) => x.id === apiFeature.id)) { + continue; + } - const dbFeature = apiFeatureToDbFeature({ - apiFeature, - }); + const dbFeature = apiFeatureToDbFeature({ + apiFeature, + }); - await createFeature({ - ctx: txCtx, - data: { - id: dbFeature.id, - name: dbFeature.name, - type: dbFeature.type, - config: dbFeature.config, - event_names: dbFeature.event_names, - }, - }); - } + await createFeature({ + ctx: txCtx, + data: { + id: dbFeature.id, + name: dbFeature.name, + type: dbFeature.type, + config: dbFeature.config, + event_names: dbFeature.event_names, + }, + }); + } - const updatedFeatures = await FeatureService.list({ - db: txDb, - orgId: org.id, - env, - }); + const updatedFeatures = await FeatureService.list({ + db: txDb, + orgId: org.id, + env, + }); - for (const apiProduct of body.products) { - if (products.some((x) => x.id === apiProduct.id)) { - continue; - } + for (const apiProduct of body.products) { + if (products.some((x) => x.id === apiProduct.id)) { + continue; + } - await createProduct({ - ctx: { - ...txCtx, - features: updatedFeatures, - }, - data: { - id: apiProduct.id, - name: apiProduct.name, - is_add_on: apiProduct.is_add_on, - is_default: apiProduct.is_default, - group: apiProduct.group, - items: apiProduct.items, - free_trial: apiProduct.free_trial, - }, - }); - } - }); + await createProduct({ + ctx: { + ...txCtx, + features: updatedFeatures, + }, + data: { + id: apiProduct.id, + name: apiProduct.name, + is_add_on: apiProduct.is_add_on, + is_default: apiProduct.is_default, + group: apiProduct.group, + items: apiProduct.items, + free_trial: apiProduct.free_trial, + }, + }); + } + }); - return c.json({ - features: body.features, - products: body.products, - }); - }, + return c.json({ + features: body.features, + products: body.products, + }); + }, }); diff --git a/server/src/internal/misc/savedViews/handlers/handleDeleteView.ts b/server/src/internal/misc/savedViews/handlers/handleDeleteView.ts index 1f48c8ca7..58c96f5b8 100644 --- a/server/src/internal/misc/savedViews/handlers/handleDeleteView.ts +++ b/server/src/internal/misc/savedViews/handlers/handleDeleteView.ts @@ -18,8 +18,7 @@ export const handleDeleteView = createRoute({ // Remove from list const listKey = `saved_views_list:${org.id}:${env}`; - const existingViews = - (await CacheManager.getJson(listKey)) || []; + const existingViews = (await CacheManager.getJson(listKey)) || []; const updatedViews = existingViews.filter((id: string) => id !== viewId); await CacheManager.setJson(listKey, updatedViews, "forever"); diff --git a/server/src/internal/orgs/onboarding/parseChatProducts.ts b/server/src/internal/orgs/onboarding/parseChatProducts.ts index b1710a522..55162fa4f 100644 --- a/server/src/internal/orgs/onboarding/parseChatProducts.ts +++ b/server/src/internal/orgs/onboarding/parseChatProducts.ts @@ -39,7 +39,11 @@ export const parseChatProducts = async ({ env: AppEnv.Sandbox, }); - const { prices, entitlements, features: updatedFeatures } = await handleNewProductItems({ + const { + prices, + entitlements, + features: updatedFeatures, + } = await handleNewProductItems({ db, curPrices: [], curEnts: [], diff --git a/server/src/internal/orgs/orgUtils/deleteOrgUtils.ts b/server/src/internal/orgs/orgUtils/deleteOrgUtils.ts index 043fb89d2..61ba7df31 100644 --- a/server/src/internal/orgs/orgUtils/deleteOrgUtils.ts +++ b/server/src/internal/orgs/orgUtils/deleteOrgUtils.ts @@ -1,9 +1,9 @@ import { AppEnv, type Organization } from "@autumn/shared"; -import type { Logger } from "@/external/logtail/logtailUtils.js"; import { deauthorizeAccount, deleteConnectedAccount, } from "@/external/connect/connectUtils.js"; +import type { Logger } from "@/external/logtail/logtailUtils.js"; import { deleteSvixApp } from "@/external/svix/svixHelpers.js"; import { deleteStripeWebhook } from "../orgUtils.js"; diff --git a/server/src/internal/products/handlers/productActions/createProduct.ts b/server/src/internal/products/handlers/productActions/createProduct.ts index f3a8c30df..e006c9f55 100644 --- a/server/src/internal/products/handlers/productActions/createProduct.ts +++ b/server/src/internal/products/handlers/productActions/createProduct.ts @@ -95,7 +95,10 @@ export const createProduct = async ({ const newFullProduct: FullProduct = { ...product, prices, - entitlements: getEntsWithFeature({ ents: entitlements, features: updatedFeatures }), + entitlements: getEntsWithFeature({ + ents: entitlements, + features: updatedFeatures, + }), free_trial: newFreeTrial, }; diff --git a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts index 513c839af..1dd12bb33 100644 --- a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts +++ b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts @@ -1,4 +1,4 @@ -import { Entitlement, Price, UsagePriceConfig } from "@autumn/shared"; +import type { Entitlement, Price, UsagePriceConfig } from "@autumn/shared"; import { Decimal } from "decimal.js"; export const getUsageFromBalance = ({ @@ -10,17 +10,17 @@ export const getUsageFromBalance = ({ price: Price; balance: number; }) => { - let config = price.config as UsagePriceConfig; - let billingUnits = config.billing_units || 1; + const config = price.config as UsagePriceConfig; + const billingUnits = config.billing_units || 1; - let overage = -Math.min(0, balance); - let roundedOverage = new Decimal(overage) + const overage = -Math.min(0, balance); + const roundedOverage = new Decimal(overage) .div(billingUnits) .ceil() .mul(billingUnits) .toNumber(); - let usage = new Decimal(ent.allowance!).sub(balance).toNumber(); + const usage = new Decimal(ent.allowance!).sub(balance).toNumber(); let roundedUsage = usage; if (overage > 0) { diff --git a/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts b/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts index 60fedde3d..6eff9ad97 100644 --- a/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts +++ b/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts @@ -1,9 +1,14 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { PriceService } from "../PriceService.js"; -import { FixedPriceConfig, prices, PriceType } from "@autumn/shared"; +import { + type FixedPriceConfig, + type Price, + PriceType, + prices, + type UsagePriceConfig, +} from "@autumn/shared"; import { eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { generateId } from "@/utils/genUtils.js"; -import { Price, UsagePriceConfig } from "@autumn/shared"; +import { PriceService } from "../PriceService.js"; export const copyPrice = async ({ db, @@ -20,7 +25,7 @@ export const copyPrice = async ({ isCustom?: boolean; withPrevConfig?: boolean; }) => { - let price = (await db.query.prices.findFirst({ + const price = (await db.query.prices.findFirst({ where: eq(prices.id, priceId), })) as Price; diff --git a/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts b/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts index 957c3ec04..f70c305c7 100644 --- a/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts +++ b/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts @@ -1,4 +1,4 @@ -import { BillingType, Price } from "@autumn/shared"; +import { BillingType, type Price } from "@autumn/shared"; import { getBillingType } from "../priceUtils.js"; const BillingTypeOrder = [ @@ -11,8 +11,8 @@ const BillingTypeOrder = [ export const sortPricesByType = (prices: Price[]) => { return prices.sort((a, b) => { - let aType = getBillingType(a.config); - let bType = getBillingType(b.config); + const aType = getBillingType(a.config); + const bType = getBillingType(b.config); return BillingTypeOrder.indexOf(aType) - BillingTypeOrder.indexOf(bType); }); }; diff --git a/server/src/internal/products/product-items/productItemUtils/convertItem.ts b/server/src/internal/products/product-items/productItemUtils/convertItem.ts index 643799dba..8a1701aff 100644 --- a/server/src/internal/products/product-items/productItemUtils/convertItem.ts +++ b/server/src/internal/products/product-items/productItemUtils/convertItem.ts @@ -1,7 +1,7 @@ import { - Feature, + type Feature, FeatureType, - ProductItem, + type ProductItem, ProductItemFeatureType, UsageModel, } from "@autumn/shared"; diff --git a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts index 06fc0ce27..893878151 100644 --- a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts +++ b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts @@ -236,11 +236,11 @@ export const toFeatureAndPrice = ({ feature_id: item.feature_id!, usage_tiers: notNullish(item.price) ? [ - { - amount: item.price, - to: TierInfinite, - }, - ] + { + amount: item.price, + to: TierInfinite, + }, + ] : (item.tiers as any), interval: itemToBillingInterval({ item }) as BillingInterval, interval_count: item.interval_count || 1, @@ -257,7 +257,7 @@ export const toFeatureAndPrice = ({ if (shouldProrate(onDecrease) || onDecrease === OnDecrease.Prorate) { onDecrease = onIncrease === OnIncrease.ProrateImmediately || - onIncrease === OnIncrease.BillImmediately + onIncrease === OnIncrease.BillImmediately ? OnDecrease.ProrateImmediately : OnDecrease.ProrateNextCycle; } diff --git a/server/src/internal/products/productRouter.ts b/server/src/internal/products/productRouter.ts index 22593307a..7f9c5c7a2 100644 --- a/server/src/internal/products/productRouter.ts +++ b/server/src/internal/products/productRouter.ts @@ -36,7 +36,10 @@ honoProductRouter.delete("/:product_id", ...handleDeleteProductHono); honoProductRouter.post("/:product_id/copy", ...handleCopyProductV2); // Info before deleting plan -honoProductRouter.get("/:product_id/has_customers", ...handlePlanHasCustomersV2); +honoProductRouter.get( + "/:product_id/has_customers", + ...handlePlanHasCustomersV2, +); honoProductRouter.post( "/:product_id/has_customers", ...handlePlanHasCustomersV2, diff --git a/server/src/middleware/pricingMiddleware.ts b/server/src/middleware/pricingMiddleware.ts index b4fd5a9e1..23e13ebbb 100644 --- a/server/src/middleware/pricingMiddleware.ts +++ b/server/src/middleware/pricingMiddleware.ts @@ -1,12 +1,15 @@ -import { FeatureId, sendProductEvent } from "@/external/autumn/autumnUtils.js"; -import { isEntitled } from "@/external/autumn/autumnUtils.js"; +import { + FeatureId, + isEntitled, + sendProductEvent, +} from "@/external/autumn/autumnUtils.js"; import { handleRequestError } from "@/utils/errorUtils.js"; export const pricingMiddleware = async (req: any, res: any, next: any) => { - let path = req.url; - let method = req.method; + const path = req.url; + const method = req.method; - let host = req.headers.host; + const host = req.headers.host; if (host.includes("localhost")) { next(); return; @@ -33,7 +36,7 @@ export const pricingMiddleware = async (req: any, res: any, next: any) => { }); } - if (path.match(/^\/products\/[^\/]+$/) && method === "DELETE") { + if (path.match(/^\/products\/[^/]+$/) && method === "DELETE") { console.log("sending product delete event"); await sendProductEvent({ org: req.org, diff --git a/server/src/middleware/publicAuthMiddleware.ts b/server/src/middleware/publicAuthMiddleware.ts index 3bdcb2296..64ac670e7 100644 --- a/server/src/middleware/publicAuthMiddleware.ts +++ b/server/src/middleware/publicAuthMiddleware.ts @@ -1,6 +1,6 @@ +import { AppEnv, AuthType, ErrCode } from "@autumn/shared"; import { verifyPublicKey } from "@/internal/dev/api-keys/publicKeyUtils.js"; import RecaseError from "@/utils/errorUtils.js"; -import { AppEnv, AuthType, ErrCode } from "@autumn/shared"; const allowedEndpoints = [ { @@ -82,7 +82,7 @@ export const verifyBearerPublishableKey = async ( }); } - let env: AppEnv = pkey.startsWith("am_pk_test") + const env: AppEnv = pkey.startsWith("am_pk_test") ? AppEnv.Sandbox : AppEnv.Live; @@ -100,7 +100,7 @@ export const verifyBearerPublishableKey = async ( }); } - let { org, features } = data; + const { org, features } = data; req.minOrg = { id: org.id, diff --git a/server/src/utils/authUtils.ts b/server/src/utils/authUtils.ts index 819956686..7827bed89 100644 --- a/server/src/utils/authUtils.ts +++ b/server/src/utils/authUtils.ts @@ -1,8 +1,9 @@ export const parseAuthHeader = (req: any) => { - let authHeader = req.headers["Authorization"] || req.headers["authorization"]; + const authHeader = + req.headers["Authorization"] || req.headers["authorization"]; if (!authHeader || !authHeader.startsWith("Bearer ")) { return null; } - let bearerToken = authHeader.split(" ")[1]; + const bearerToken = authHeader.split(" ")[1]; return bearerToken; }; diff --git a/server/src/utils/checkUtils/allCusProductsOnSubFree.ts b/server/src/utils/checkUtils/allCusProductsOnSubFree.ts index e403e2fbe..f84a8f1ea 100644 --- a/server/src/utils/checkUtils/allCusProductsOnSubFree.ts +++ b/server/src/utils/checkUtils/allCusProductsOnSubFree.ts @@ -4,7 +4,7 @@ import { isFreeProduct, } from "@autumn/shared"; -export const allCusProductsOnSubFree = ({ +export const allCusProductsOnSubFree = ({ fullCus, subId, }: { diff --git a/server/src/utils/checkUtils/checkCusProducts.ts b/server/src/utils/checkUtils/checkCusProducts.ts index 3524391c2..d450c2fa4 100644 --- a/server/src/utils/checkUtils/checkCusProducts.ts +++ b/server/src/utils/checkUtils/checkCusProducts.ts @@ -1,8 +1,8 @@ import { - CusProductStatus, - cusProductToPrices, - type FullCusProduct, - isFreeProduct, + CusProductStatus, + cusProductToPrices, + type FullCusProduct, + isFreeProduct, } from "@autumn/shared"; import type { FullCustomer } from "../../../../shared/models/cusModels/fullCusModel"; import { getRelatedCusPrice } from "../../internal/customers/cusProducts/cusEnts/cusEntUtils"; diff --git a/server/src/utils/checkUtils/checkSubCountMatch.ts b/server/src/utils/checkUtils/checkSubCountMatch.ts index dec248b04..ed63ad505 100644 --- a/server/src/utils/checkUtils/checkSubCountMatch.ts +++ b/server/src/utils/checkUtils/checkSubCountMatch.ts @@ -45,9 +45,6 @@ export const checkSubCountMatch = async ({ .from(metadata) .where(inArray(metadata.stripe_invoice_id, invoiceIds as string[])); - - - if ( stripeSubs.length - (metadataCount?.[0]?.count || 0) !== subIds.length diff --git a/server/src/utils/checkUtils/saveCheckState.ts b/server/src/utils/checkUtils/saveCheckState.ts index f86304bfd..682ff9ac0 100644 --- a/server/src/utils/checkUtils/saveCheckState.ts +++ b/server/src/utils/checkUtils/saveCheckState.ts @@ -17,22 +17,28 @@ export const saveCheckState = async ({ result: StateCheckResult; }): Promise => { const newFailedChecks = result.checks - .filter((c): c is typeof c & { type: CheckType } => c.type !== "overall_status") + .filter( + (c): c is typeof c & { type: CheckType } => c.type !== "overall_status", + ) .filter((c) => !c.passed); // Skip Redis entirely if all checks pass if (newFailedChecks.length === 0) return; const stateKey = `state:${org.id}:${env}:${fullCus.internal_id}`; - - const existingState = (await upstash.get(stateKey)) as RedisChecksState | null; + + const existingState = (await upstash.get( + stateKey, + )) as RedisChecksState | null; if (existingState) { // Merge with existing state - const existingFailedTypes = new Set(existingState.checks.map((c) => c.type)); - const newFailedTypes = new Set(newFailedChecks.map((c) => c.type)); - - + const existingFailedTypes = new Set( + existingState.checks.map((c) => c.type), + ); + const newFailedTypes = new Set( + newFailedChecks.map((c) => c.type), + ); // Checks that were fixed (existed before but not now) const checksToRemove = new Set( @@ -44,7 +50,9 @@ export const saveCheckState = async ({ ); // Keep checks that still fail, remove ones that are fixed - const updatedChecks = existingState.checks.filter((c) => !checksToRemove.has(c.type)); + const updatedChecks = existingState.checks.filter( + (c) => !checksToRemove.has(c.type), + ); // Add new failing checks for (const check of newFailedChecks) { @@ -65,7 +73,6 @@ export const saveCheckState = async ({ checks: updatedChecks, }; await upstash.set(stateKey, JSON.stringify(updatedState)); - } else { // Create new state const newState: RedisChecksState = { @@ -87,7 +94,5 @@ export const saveCheckState = async ({ })), }; await upstash.set(stateKey, JSON.stringify(newState)); - } }; - diff --git a/server/src/utils/encryptUtils.ts b/server/src/utils/encryptUtils.ts index f0a6f744b..a2d735838 100644 --- a/server/src/utils/encryptUtils.ts +++ b/server/src/utils/encryptUtils.ts @@ -50,6 +50,6 @@ export const generatePublishableKey = (env: string) => { // .replace(/[+/=]/g, "") // Remove non-URL safe characters // .slice(0, 100); // Ensure exactly 100 characters - let envString = env === AppEnv.Sandbox ? "test_" : "live_"; + const envString = env === AppEnv.Sandbox ? "test_" : "live_"; return `am_pk_${envString}${KSUID.randomSync().string}`; }; diff --git a/server/src/utils/initUtils.ts b/server/src/utils/initUtils.ts index 8c9702d28..0b9a6c669 100644 --- a/server/src/utils/initUtils.ts +++ b/server/src/utils/initUtils.ts @@ -19,8 +19,6 @@ export const checkEnvVars = () => { process.exit(1); } - - if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) { console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`); process.exit(1); diff --git a/server/src/utils/models/ItemSet.ts b/server/src/utils/models/ItemSet.ts index 01eb0eaa0..0ce0cd9bb 100644 --- a/server/src/utils/models/ItemSet.ts +++ b/server/src/utils/models/ItemSet.ts @@ -1,4 +1,4 @@ -import { type Price } from "@autumn/shared"; +import type { Price } from "@autumn/shared"; import type Stripe from "stripe"; // export const ItemSetSchema = z.object({ diff --git a/server/src/utils/scriptUtils/clearOrg.ts b/server/src/utils/scriptUtils/clearOrg.ts index 7514fc32b..85851a970 100644 --- a/server/src/utils/scriptUtils/clearOrg.ts +++ b/server/src/utils/scriptUtils/clearOrg.ts @@ -1,12 +1,12 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; import { AppEnv, customers, features, - Organization, + type Organization, products, } from "@autumn/shared"; import { and, eq, inArray } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; export const clearCustomersInBatches = async ({ db, diff --git a/server/src/utils/scriptUtils/genScriptUtils.ts b/server/src/utils/scriptUtils/genScriptUtils.ts index 5a3af1a27..b367b5d25 100644 --- a/server/src/utils/scriptUtils/genScriptUtils.ts +++ b/server/src/utils/scriptUtils/genScriptUtils.ts @@ -11,7 +11,7 @@ export const parseCsv = ({ return new Promise((resolve, reject) => { const stream = fs.createReadStream(path); const results: any[] = []; - let headers: string[] = []; + const headers: string[] = []; stream .pipe(csv({ separator: delimiter })) .on("data", (data) => { diff --git a/server/src/utils/scriptUtils/getAll/getAllCusProds.ts b/server/src/utils/scriptUtils/getAll/getAllCusProds.ts index f4e6ce9c7..6c550394a 100644 --- a/server/src/utils/scriptUtils/getAll/getAllCusProds.ts +++ b/server/src/utils/scriptUtils/getAll/getAllCusProds.ts @@ -1,8 +1,8 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { AppEnv, FullCusProduct } from "@autumn/shared"; +import { AppEnv, type FullCusProduct } from "@autumn/shared"; import { sql } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; -let cusProductsQuery = ({ +const cusProductsQuery = ({ lastProductId, internalProductId, pageSize = 250, @@ -87,8 +87,8 @@ export const getAllFullCusProducts = async ({ internalProductId: string; }) => { let lastProductId = ""; - let allData: any[] = []; - let pageSize = 500; + const allData: any[] = []; + const pageSize = 500; while (true) { const data = await db.execute( diff --git a/server/src/utils/scriptUtils/getAll/getAllUsers.ts b/server/src/utils/scriptUtils/getAll/getAllUsers.ts index 101c895fc..90feab40c 100644 --- a/server/src/utils/scriptUtils/getAll/getAllUsers.ts +++ b/server/src/utils/scriptUtils/getAll/getAllUsers.ts @@ -1,7 +1,6 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; import { user } from "@autumn/shared"; - import { desc } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; export const getAllUsers = async (db: DrizzleCli) => { const users = []; diff --git a/server/src/utils/scriptUtils/resetOrgStripe.ts b/server/src/utils/scriptUtils/resetOrgStripe.ts index 6b13789cc..2da3e5beb 100644 --- a/server/src/utils/scriptUtils/resetOrgStripe.ts +++ b/server/src/utils/scriptUtils/resetOrgStripe.ts @@ -1,9 +1,9 @@ -import { Organization, AppEnv } from "@autumn/shared"; +import { AppEnv, type Organization } from "@autumn/shared"; +import { deleteAllStripeCustomers } from "@/external/stripe/stripeCusUtils.js"; import { deactivateStripeMeters, deleteAllStripeProducts, } from "@/external/stripe/stripeProductUtils.js"; -import { deleteAllStripeCustomers } from "@/external/stripe/stripeCusUtils.js"; export const resetOrgStripe = async ({ org }: { org: Organization }) => { const env = AppEnv.Sandbox; diff --git a/server/src/utils/timeUtils.ts b/server/src/utils/timeUtils.ts index 30206add5..a758146b9 100644 --- a/server/src/utils/timeUtils.ts +++ b/server/src/utils/timeUtils.ts @@ -1,8 +1,6 @@ -import { Duration, EntInterval } from "@autumn/shared"; - -import { Entitlement } from "@autumn/shared"; -import { add } from "date-fns"; +import { Duration, EntInterval, Entitlement } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; +import { add } from "date-fns"; import { formatUnixToDate } from "./genUtils.js"; // Time conversion constants diff --git a/server/tests/advanced/customInterval/customInterval1.test.ts b/server/tests/advanced/customInterval/customInterval1.test.ts index 1305acc80..d2eb89e38 100644 --- a/server/tests/advanced/customInterval/customInterval1.test.ts +++ b/server/tests/advanced/customInterval/customInterval1.test.ts @@ -1,13 +1,13 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; -import { addHours, addMonths } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "@tests/utils/constants.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/advanced/customInterval/customInterval3.test.ts b/server/tests/advanced/customInterval/customInterval3.test.ts index a4d7012fb..dd466c2ee 100644 --- a/server/tests/advanced/customInterval/customInterval3.test.ts +++ b/server/tests/advanced/customInterval/customInterval3.test.ts @@ -1,12 +1,12 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; -import { addDays, addMonths } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addDays, addMonths } from "date-fns"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; import { diff --git a/server/tests/advanced/customInterval/customInterval4.test.ts b/server/tests/advanced/customInterval/customInterval4.test.ts index a4de915eb..2c8b74865 100644 --- a/server/tests/advanced/customInterval/customInterval4.test.ts +++ b/server/tests/advanced/customInterval/customInterval4.test.ts @@ -1,8 +1,5 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; -import { addMonths } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { @@ -11,6 +8,9 @@ import { } from "@tests/utils/expectUtils/expectScheduleUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js"; +import chalk from "chalk"; +import { addMonths } from "date-fns"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/advanced/multiFeature/multiFeature1.test.ts b/server/tests/advanced/multiFeature/multiFeature1.test.ts index 44a12ca8e..4728e0d9c 100644 --- a/server/tests/advanced/multiFeature/multiFeature1.test.ts +++ b/server/tests/advanced/multiFeature/multiFeature1.test.ts @@ -1,6 +1,5 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, type AppEnv, LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import { getPrepaidCusEnt, @@ -8,6 +7,7 @@ import { } from "@tests/utils/cusProductUtils/cusEntSearchUtils.js"; import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; diff --git a/server/tests/advanced/multiFeature/multiFeature2.test.ts b/server/tests/advanced/multiFeature/multiFeature2.test.ts index 0a243dcbd..342cfcced 100644 --- a/server/tests/advanced/multiFeature/multiFeature2.test.ts +++ b/server/tests/advanced/multiFeature/multiFeature2.test.ts @@ -1,6 +1,5 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, type AppEnv, LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import { getLifetimeFreeCusEnt, @@ -8,6 +7,7 @@ import { } from "@tests/utils/cusProductUtils/cusEntSearchUtils.js"; import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; diff --git a/server/tests/advanced/multiFeature/multiFeature3.test.ts b/server/tests/advanced/multiFeature/multiFeature3.test.ts index fefd01f37..60297e901 100644 --- a/server/tests/advanced/multiFeature/multiFeature3.test.ts +++ b/server/tests/advanced/multiFeature/multiFeature3.test.ts @@ -2,8 +2,6 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, type AppEnv } from "@autumn/shared"; -import chalk from "chalk"; -import { addMonths } from "date-fns"; import { TestFeature } from "@tests/setup/v2Features.js"; import { getLifetimeFreeCusEnt, @@ -11,6 +9,8 @@ import { } from "@tests/utils/cusProductUtils/cusEntSearchUtils.js"; import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addMonths } from "date-fns"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; diff --git a/server/tests/advanced/usage/sharedProducts.ts b/server/tests/advanced/usage/sharedProducts.ts index f15ec74fb..ac79eb1f8 100644 --- a/server/tests/advanced/usage/sharedProducts.ts +++ b/server/tests/advanced/usage/sharedProducts.ts @@ -3,8 +3,8 @@ import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { - constructFeatureItem, constructArrearItem, + constructFeatureItem, } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js"; diff --git a/server/tests/advanced/usage/usage1.test.ts b/server/tests/advanced/usage/usage1.test.ts index 861301f00..3165614b0 100644 --- a/server/tests/advanced/usage/usage1.test.ts +++ b/server/tests/advanced/usage/usage1.test.ts @@ -5,12 +5,12 @@ import { ProductItemInterval, UsageModel, } from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js"; import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/advanced/usageLimit/usageLimit1.test.ts b/server/tests/advanced/usageLimit/usageLimit1.test.ts index ae4291292..b5b7ca9fe 100644 --- a/server/tests/advanced/usageLimit/usageLimit1.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit1.test.ts @@ -1,10 +1,10 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ErrCode, LegacyVersion, type LimitedItem } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/advanced/usageLimit/usageLimit2.test.ts b/server/tests/advanced/usageLimit/usageLimit2.test.ts index be1c3f5e9..054fddb6d 100644 --- a/server/tests/advanced/usageLimit/usageLimit2.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit2.test.ts @@ -1,10 +1,10 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion, type LimitedItem } from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { diff --git a/server/tests/advanced/usageLimit/usageLimit3.test.ts b/server/tests/advanced/usageLimit/usageLimit3.test.ts index 1b9309634..775108229 100644 --- a/server/tests/advanced/usageLimit/usageLimit3.test.ts +++ b/server/tests/advanced/usageLimit/usageLimit3.test.ts @@ -1,11 +1,11 @@ import { beforeAll, describe, test } from "bun:test"; import { ErrCode, LegacyVersion, type LimitedItem } from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/archives/contUse/update/updateContUse1.test.ts b/server/tests/archives/contUse/update/updateContUse1.test.ts index 8e9c04cfe..5db884462 100644 --- a/server/tests/archives/contUse/update/updateContUse1.test.ts +++ b/server/tests/archives/contUse/update/updateContUse1.test.ts @@ -1,14 +1,14 @@ -import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { replaceItems } from "@tests/attach/utils.js"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/archives/contUse/update/updateContUse3.test.ts b/server/tests/archives/contUse/update/updateContUse3.test.ts index 765de4eea..d2c029652 100644 --- a/server/tests/archives/contUse/update/updateContUse3.test.ts +++ b/server/tests/archives/contUse/update/updateContUse3.test.ts @@ -1,12 +1,12 @@ -import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { beforeAll, describe, test } from "bun:test"; -import chalk from "chalk"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { replaceItems } from "@tests/attach/utils.js"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { attachNewContUseAndExpectCorrect } from "@tests/utils/expectUtils/expectContUse/expectUpdateContUse.js"; import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/archives/contUse/update/updateContUse4.test.ts b/server/tests/archives/contUse/update/updateContUse4.test.ts index c74dbae5f..0db256e91 100644 --- a/server/tests/archives/contUse/update/updateContUse4.test.ts +++ b/server/tests/archives/contUse/update/updateContUse4.test.ts @@ -1,14 +1,14 @@ -import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { replaceItems } from "@tests/attach/utils.js"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { attachNewContUseAndExpectCorrect } from "@tests/utils/expectUtils/expectContUse/expectUpdateContUse.js"; import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; diff --git a/server/tests/archives/contUse/update/updateContUse5.test.ts b/server/tests/archives/contUse/update/updateContUse5.test.ts index 2950cea54..79bd8c105 100644 --- a/server/tests/archives/contUse/update/updateContUse5.test.ts +++ b/server/tests/archives/contUse/update/updateContUse5.test.ts @@ -1,11 +1,11 @@ -import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { beforeAll, describe, test } from "bun:test"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/attach/addOn/addOn2.test.ts b/server/tests/attach/addOn/addOn2.test.ts index 6b4276851..d6c20c4b3 100644 --- a/server/tests/attach/addOn/addOn2.test.ts +++ b/server/tests/attach/addOn/addOn2.test.ts @@ -1,12 +1,12 @@ import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/attach/checkout/checkout1.test.ts b/server/tests/attach/checkout/checkout1.test.ts index c11bec24d..61d2dd829 100644 --- a/server/tests/attach/checkout/checkout1.test.ts +++ b/server/tests/attach/checkout/checkout1.test.ts @@ -5,13 +5,13 @@ import { type Entitlement, ProductItemInterval, } from "@autumn/shared"; -import chalk from "chalk"; import { AutumnCli } from "@tests/cli/AutumnCli.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js"; import { timeout } from "@tests/utils/genUtils.js"; import { completeCheckoutForm } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; diff --git a/server/tests/attach/checkout/checkout8.test.ts b/server/tests/attach/checkout/checkout8.test.ts index b9fb3c961..90ff9a5eb 100644 --- a/server/tests/attach/checkout/checkout8.test.ts +++ b/server/tests/attach/checkout/checkout8.test.ts @@ -1,12 +1,12 @@ import { beforeAll, describe, test } from "bun:test"; import { ApiVersion } from "@autumn/shared"; -import chalk from "chalk"; import { AutumnCli } from "@tests/cli/AutumnCli.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js"; import { timeout } from "@tests/utils/genUtils.js"; import { completeCheckoutForm } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/attach/downgrade/downgrade2.test.ts b/server/tests/attach/downgrade/downgrade2.test.ts index efd6e8d91..f9386106c 100644 --- a/server/tests/attach/downgrade/downgrade2.test.ts +++ b/server/tests/attach/downgrade/downgrade2.test.ts @@ -1,7 +1,5 @@ import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { @@ -9,6 +7,8 @@ import { expectNextCycleCorrect, } from "@tests/utils/expectUtils/expectScheduleUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js"; diff --git a/server/tests/attach/migrations/runMigrationTest.ts b/server/tests/attach/migrations/runMigrationTest.ts index 1358e85a6..b8cf29226 100644 --- a/server/tests/attach/migrations/runMigrationTest.ts +++ b/server/tests/attach/migrations/runMigrationTest.ts @@ -1,20 +1,20 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { ProductV2, Organization } from "@autumn/shared"; -import { AppEnv } from "autumn-js"; +import { expect } from "bun:test"; +import type { Organization, ProductV2 } from "@autumn/shared"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js"; +import { expectResetAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectResetAtCorrect.js"; +import { expectTrialEndsAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js"; +import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js"; import { expectSubItemsCorrect, getSubsFromCusId, } from "@tests/utils/expectUtils/expectSubUtils.js"; -import Stripe from "stripe"; -import { expect } from "bun:test"; -import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js"; -import { expectResetAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectResetAtCorrect.js"; -import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; -import { expectTrialEndsAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js"; -import { timeout } from "@/utils/genUtils.js"; +import type { AppEnv } from "autumn-js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnInt } from "@/external/autumn/autumnCli.js"; import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js"; +import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; +import { timeout } from "@/utils/genUtils.js"; export const expectSubsSame = ({ subsBefore, @@ -25,8 +25,8 @@ export const expectSubsSame = ({ }) => { // let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice); // let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice); - let subIdsBefore = subsBefore.map((sub) => sub.id); - let subIdsAfter = subsAfter.map((sub) => sub.id); + const subIdsBefore = subsBefore.map((sub) => sub.id); + const subIdsAfter = subsAfter.map((sub) => sub.id); const periodsBefore = subsBefore.map((sub) => subToPeriodStartEnd({ sub })); const periodsAfter = subsAfter.map((sub) => subToPeriodStartEnd({ sub })); diff --git a/server/tests/attach/newVersion/newVersion2.test.ts b/server/tests/attach/newVersion/newVersion2.test.ts index 19d37b6e0..20a7dabd6 100644 --- a/server/tests/attach/newVersion/newVersion2.test.ts +++ b/server/tests/attach/newVersion/newVersion2.test.ts @@ -1,13 +1,9 @@ -import { - BillingInterval, - LegacyVersion, - type ProductV2, -} from "@autumn/shared"; import { beforeAll, describe, test } from "bun:test"; -import chalk from "chalk"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { BillingInterval, LegacyVersion, type ProductV2 } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/attach/others/others1.test.ts b/server/tests/attach/others/others1.test.ts index 57b4af188..a6066f7c8 100644 --- a/server/tests/attach/others/others1.test.ts +++ b/server/tests/attach/others/others1.test.ts @@ -1,18 +1,18 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectDowngradeCorrect, expectNextCycleCorrect, } from "@tests/utils/expectUtils/expectScheduleUtils.js"; -import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const testCase = "others1"; diff --git a/server/tests/attach/others/others10.test.ts b/server/tests/attach/others/others10.test.ts index e3cd68355..8ec0d3826 100644 --- a/server/tests/attach/others/others10.test.ts +++ b/server/tests/attach/others/others10.test.ts @@ -11,83 +11,83 @@ import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js" import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const pro = constructProduct({ - type: "pro", - items: [ - constructFeatureItem({ - featureId: TestFeature.Credits, - includedUsage: 500, - }), - ], + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Credits, + includedUsage: 500, + }), + ], }); const testCase = "others10"; describe(`${chalk.yellowBright(`${testCase}/idempotency: idempotency key already exists`)}`, () => { - const customerId = testCase; - const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); - const idempotencyKey = generateId("it"); + const customerId = testCase; + const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); + const idempotencyKey = generateId("it"); - let results: PromiseSettledResult< - Awaited> - >[]; + let results: PromiseSettledResult< + Awaited> + >[]; - beforeAll(async () => { - await initCustomerV3({ - ctx, - customerId, - withTestClock: true, - attachPm: "success", - }); + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + attachPm: "success", + }); - await initProductsV0({ - ctx, - products: [pro], - prefix: testCase, - }); + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); - results = await Promise.allSettled([ - autumnV1.attach( - { - customer_id: customerId, - product_id: pro.id, - }, - { - "idempotency-key": idempotencyKey, - }, - ), - autumnV1.attach( - { - customer_id: customerId, - product_id: pro.id, - }, - { - "idempotency-key": idempotencyKey, - }, - ), - ]); - }); + results = await Promise.allSettled([ + autumnV1.attach( + { + customer_id: customerId, + product_id: pro.id, + }, + { + "idempotency-key": idempotencyKey, + }, + ), + autumnV1.attach( + { + customer_id: customerId, + product_id: pro.id, + }, + { + "idempotency-key": idempotencyKey, + }, + ), + ]); + }); - test("should reject duplicate idempotency key with 409", async () => { - // Exactly one request should succeed - const fulfilled = results.filter((r) => r.status === "fulfilled"); - const rejected = results.filter((r) => r.status === "rejected"); + test("should reject duplicate idempotency key with 409", async () => { + // Exactly one request should succeed + const fulfilled = results.filter((r) => r.status === "fulfilled"); + const rejected = results.filter((r) => r.status === "rejected"); - expect(fulfilled).toHaveLength(1); - expect(rejected).toHaveLength(1); + expect(fulfilled).toHaveLength(1); + expect(rejected).toHaveLength(1); - // The successful request should have attached the product - const successResult = fulfilled[0] as PromiseFulfilledResult< - Awaited> - >; - expect(successResult.value.success).toBe(true); - expect(successResult.value.customer_id).toBe(customerId); - expect(successResult.value.product_ids).toContain(pro.id); + // The successful request should have attached the product + const successResult = fulfilled[0] as PromiseFulfilledResult< + Awaited> + >; + expect(successResult.value.success).toBe(true); + expect(successResult.value.customer_id).toBe(customerId); + expect(successResult.value.product_ids).toContain(pro.id); - // The rejected request should have the duplicate idempotency key error - const rejectedResult = rejected[0] as PromiseRejectedResult; - expect(rejectedResult.reason).toBeInstanceOf(AutumnError); - expect((rejectedResult.reason as AutumnError).code).toBe( - ErrCode.DuplicateIdempotencyKey, - ); - }); + // The rejected request should have the duplicate idempotency key error + const rejectedResult = rejected[0] as PromiseRejectedResult; + expect(rejectedResult.reason).toBeInstanceOf(AutumnError); + expect((rejectedResult.reason as AutumnError).code).toBe( + ErrCode.DuplicateIdempotencyKey, + ); + }); }); diff --git a/server/tests/attach/others/others2.test.ts b/server/tests/attach/others/others2.test.ts index c58da7844..623e0bb04 100644 --- a/server/tests/attach/others/others2.test.ts +++ b/server/tests/attach/others/others2.test.ts @@ -1,9 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; diff --git a/server/tests/attach/others/others3.test.ts b/server/tests/attach/others/others3.test.ts index e6329133d..220da93ae 100644 --- a/server/tests/attach/others/others3.test.ts +++ b/server/tests/attach/others/others3.test.ts @@ -1,15 +1,15 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; -import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const testCase = "others3"; diff --git a/server/tests/attach/others/others7.test.ts b/server/tests/attach/others/others7.test.ts index b1b9f42e3..404dca297 100644 --- a/server/tests/attach/others/others7.test.ts +++ b/server/tests/attach/others/others7.test.ts @@ -1,17 +1,17 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectAttachCorrect } from "@tests/utils/expectUtils/expectAttach.js"; -import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; export const pro = constructProduct({ items: [constructArrearItem({ featureId: TestFeature.Words })], diff --git a/server/tests/attach/others/others8.test.ts b/server/tests/attach/others/others8.test.ts index bea79292a..34edde01a 100644 --- a/server/tests/attach/others/others8.test.ts +++ b/server/tests/attach/others/others8.test.ts @@ -1,12 +1,10 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js"; import chalk from "chalk"; import type Stripe from "stripe"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; -import { TestFeature } from "@tests/setup/v2Features.js"; -import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; -import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { @@ -14,6 +12,8 @@ import { constructPrepaidItem, } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; export const pro = constructProduct({ items: [ diff --git a/server/tests/attach/others/others9.test.ts b/server/tests/attach/others/others9.test.ts index fe191e873..fe876b234 100644 --- a/server/tests/attach/others/others9.test.ts +++ b/server/tests/attach/others/others9.test.ts @@ -1,17 +1,17 @@ -import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import type Stripe from "stripe"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js"; -import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; export const free = constructProduct({ items: [ diff --git a/server/tests/attach/updateEnts/updateEnts4.test.ts b/server/tests/attach/updateEnts/updateEnts4.test.ts index 66a1864b5..9a2c18b3d 100644 --- a/server/tests/attach/updateEnts/updateEnts4.test.ts +++ b/server/tests/attach/updateEnts/updateEnts4.test.ts @@ -1,13 +1,9 @@ -import { - AttachBranch, - BillingInterval, - LegacyVersion, -} from "@autumn/shared"; import { beforeAll, describe, expect, test } from "bun:test"; -import chalk from "chalk"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import { AttachBranch, BillingInterval, LegacyVersion } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { nullish } from "@/utils/genUtils.js"; diff --git a/server/tests/attach/updateQuantity/updateQuantity1.test.ts b/server/tests/attach/updateQuantity/updateQuantity1.test.ts index 4ed6c56b7..3e0fcc41c 100644 --- a/server/tests/attach/updateQuantity/updateQuantity1.test.ts +++ b/server/tests/attach/updateQuantity/updateQuantity1.test.ts @@ -5,9 +5,6 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js"; @@ -15,6 +12,9 @@ import { createProducts } from "@tests/utils/productUtils.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import { addPrefixToProducts } from "@tests/utils/testProductUtils/testProductUtils.js"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; diff --git a/server/tests/attach/upgrade/upgrade4.test.ts b/server/tests/attach/upgrade/upgrade4.test.ts index 080bfbac3..7c1c0fa87 100644 --- a/server/tests/attach/upgrade/upgrade4.test.ts +++ b/server/tests/attach/upgrade/upgrade4.test.ts @@ -1,12 +1,12 @@ import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/attach/upgrade/upgrade5.test.ts b/server/tests/attach/upgrade/upgrade5.test.ts index 060452727..41c7297b4 100644 --- a/server/tests/attach/upgrade/upgrade5.test.ts +++ b/server/tests/attach/upgrade/upgrade5.test.ts @@ -1,12 +1,12 @@ import { beforeAll, describe, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; -import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/attach/upgradeOld/upgradeOld3.test.ts b/server/tests/attach/upgradeOld/upgradeOld3.test.ts index 5abba4318..e4087b540 100644 --- a/server/tests/attach/upgradeOld/upgradeOld3.test.ts +++ b/server/tests/attach/upgradeOld/upgradeOld3.test.ts @@ -5,12 +5,12 @@ import { FreeTrialDuration, ProductItemInterval, } from "@autumn/shared"; -import chalk from "chalk"; -import { addDays } from "date-fns"; -import type Stripe from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addDays } from "date-fns"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/attach/upgradeOld/upgradeOld4.test.ts b/server/tests/attach/upgradeOld/upgradeOld4.test.ts index 4d9178d29..21320435d 100644 --- a/server/tests/attach/upgradeOld/upgradeOld4.test.ts +++ b/server/tests/attach/upgradeOld/upgradeOld4.test.ts @@ -6,13 +6,13 @@ import { type Customer, ProductItemInterval, } from "@autumn/shared"; -import chalk from "chalk"; -import type Stripe from "stripe"; import { AutumnCli } from "@tests/cli/AutumnCli.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js"; import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type Stripe from "stripe"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { attachFailedPaymentMethod, diff --git a/server/tests/balances/check/loose/entities/entity-loose-4.test.ts b/server/tests/balances/check/loose/entities/entity-loose-4.test.ts index 833c3d7d9..9849d45b7 100644 --- a/server/tests/balances/check/loose/entities/entity-loose-4.test.ts +++ b/server/tests/balances/check/loose/entities/entity-loose-4.test.ts @@ -1,5 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; -import { ApiVersion, type CheckResponseV2, ResetInterval } from "@autumn/shared"; +import { + ApiVersion, + type CheckResponseV2, + ResetInterval, +} from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; diff --git a/server/tests/balances/check/loose/entities/entity-loose-6.test.ts b/server/tests/balances/check/loose/entities/entity-loose-6.test.ts index 35bd6f01a..8e2dd0e3b 100644 --- a/server/tests/balances/check/loose/entities/entity-loose-6.test.ts +++ b/server/tests/balances/check/loose/entities/entity-loose-6.test.ts @@ -86,7 +86,9 @@ describe(`${chalk.yellowBright(`${testCase}: customer loose + entity loose isola expect(breakdown).toHaveLength(2); // Both should be loose (plan_id null) - const balances = breakdown?.map((b) => b.granted_balance).sort((a, b) => a - b); + const balances = breakdown + ?.map((b) => b.granted_balance) + .sort((a, b) => a - b); expect(balances).toEqual([200, 300]); }); diff --git a/server/tests/balances/check/loose/loose-3.test.ts b/server/tests/balances/check/loose/loose-3.test.ts index 7a5f04cb8..3d0675b80 100644 --- a/server/tests/balances/check/loose/loose-3.test.ts +++ b/server/tests/balances/check/loose/loose-3.test.ts @@ -69,7 +69,9 @@ describe(`${chalk.yellowBright("check-loose3: mixed product + loose entitlement" expect(res.balance?.current_balance).toBe(600); // When mixed sources, plan_id should be null and breakdown should exist - expect(res.balance?.breakdown?.find((b) => b.plan_id === null)).toBeDefined(); + expect( + res.balance?.breakdown?.find((b) => b.plan_id === null), + ).toBeDefined(); expect(res.balance?.breakdown).toBeDefined(); expect(res.balance?.breakdown).toHaveLength(2); }); diff --git a/server/tests/balances/check/loose/loose-4.test.ts b/server/tests/balances/check/loose/loose-4.test.ts index a0b5e84a9..207237e66 100644 --- a/server/tests/balances/check/loose/loose-4.test.ts +++ b/server/tests/balances/check/loose/loose-4.test.ts @@ -1,5 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; -import { ApiVersion, type CheckResponseV2, ResetInterval } from "@autumn/shared"; +import { + ApiVersion, + type CheckResponseV2, + ResetInterval, +} from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; @@ -70,6 +74,5 @@ describe(`${chalk.yellowBright("check-loose4: loose entitlement with reset inter expect(res.balance?.reset).toBeDefined(); expect(res.balance?.reset?.interval).toBe(ResetInterval.Month); expect(res.balance?.reset?.resets_at).toBeDefined(); - }); }); diff --git a/server/tests/balances/check/prepaid/check-prepaid2.test.ts b/server/tests/balances/check/prepaid/check-prepaid2.test.ts index e788a1b46..a41bc99ee 100644 --- a/server/tests/balances/check/prepaid/check-prepaid2.test.ts +++ b/server/tests/balances/check/prepaid/check-prepaid2.test.ts @@ -181,7 +181,8 @@ describe(`${chalk.yellowBright("check-prepaid2: test /check on prepaid + pay per expect.objectContaining({ granted_balance: prepaidItem.included_usage, purchased_balance: prepaidQuantity, - current_balance: prepaidQuantity + prepaidItem.included_usage - curUsage, + current_balance: + prepaidQuantity + prepaidItem.included_usage - curUsage, usage: curUsage, overage_allowed: false, }), diff --git a/server/tests/balances/track/allocated/track-allocated4.test.ts b/server/tests/balances/track/allocated/track-allocated4.test.ts index efc7d2c97..5cfae1fdf 100644 --- a/server/tests/balances/track/allocated/track-allocated4.test.ts +++ b/server/tests/balances/track/allocated/track-allocated4.test.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, ProductItemFeatureType } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/balances/track/basic/track-basic2.test.ts b/server/tests/balances/track/basic/track-basic2.test.ts index 33bc8b287..11eea056b 100644 --- a/server/tests/balances/track/basic/track-basic2.test.ts +++ b/server/tests/balances/track/basic/track-basic2.test.ts @@ -1,9 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, type TrackResponseV2 } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import { timeout } from "@tests/utils/genUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/balances/track/breakdown/track-breakdown-customer-and-entity-product.test.ts b/server/tests/balances/track/breakdown/track-breakdown-customer-and-entity-product.test.ts index a702640b8..41a2255ab 100644 --- a/server/tests/balances/track/breakdown/track-breakdown-customer-and-entity-product.test.ts +++ b/server/tests/balances/track/breakdown/track-breakdown-customer-and-entity-product.test.ts @@ -18,7 +18,7 @@ import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js" * Test 3.2: Customer has product + Entity also has same product * - Product A: 100 messages (attached to customer) * - Product A: 100 messages (attached to entity) - * + * * Expected: * - Customer level: 200 total, 2 breakdown items (one customer-level, one entity-level) * - Entity level: 200 total (inherits customer + own), 2 breakdown items @@ -43,7 +43,11 @@ describe(`${chalk.yellowBright("track-breakdown-cus-and-entity-prod: customer + const customerId = testCase; const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 }); - const entity = { id: `${testCase}-user-1`, name: "User 1", feature_id: TestFeature.Users }; + const entity = { + id: `${testCase}-user-1`, + name: "User 1", + feature_id: TestFeature.Users, + }; beforeAll(async () => { await initCustomerV3({ @@ -170,8 +174,11 @@ describe(`${chalk.yellowBright("track-breakdown-cus-and-entity-prod: customer + expect(customerRes.balance?.current_balance).toBe(30); // Breakdown sum should match - const sum = customerRes.balance?.breakdown?.reduce((s, b) => s + (b.current_balance ?? 0), 0) ?? 0; + const sum = + customerRes.balance?.breakdown?.reduce( + (s, b) => s + (b.current_balance ?? 0), + 0, + ) ?? 0; expect(sum).toBe(30); }); }); - diff --git a/server/tests/balances/track/breakdown/track-breakdown-mixed-entity.test.ts b/server/tests/balances/track/breakdown/track-breakdown-mixed-entity.test.ts index ac9c9435c..2cda541b9 100644 --- a/server/tests/balances/track/breakdown/track-breakdown-mixed-entity.test.ts +++ b/server/tests/balances/track/breakdown/track-breakdown-mixed-entity.test.ts @@ -19,7 +19,7 @@ import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js" * - Product A: 100 messages per-entity (attached to customer, shared entitlement) * - Product B: 50 messages (attached directly to entity-1) * - Product C: 50 messages (attached directly to entity-2) - * + * * Expected breakdown at customer level: * - 1 breakdown for per-entity product (shared customer_entitlement_id, aggregated) * - 1 breakdown for entity-1's product (unique customer_entitlement_id) @@ -119,7 +119,9 @@ describe(`${chalk.yellowBright("track-breakdown-mixed-entity: per-entity + entit expect(res.balance?.breakdown).toHaveLength(3); const breakdowns = res.balance?.breakdown ?? []; - const balances = breakdowns.map((b) => b.granted_balance).sort((a, b) => (a ?? 0) - (b ?? 0)); + const balances = breakdowns + .map((b) => b.granted_balance) + .sort((a, b) => (a ?? 0) - (b ?? 0)); expect(balances).toEqual([50, 50, 200]); // All IDs should be unique (different customer_entitlement_ids) @@ -148,7 +150,9 @@ describe(`${chalk.yellowBright("track-breakdown-mixed-entity: per-entity + entit // Entity should have 2 breakdowns expect(res.balance?.breakdown).toHaveLength(2); - const balances = res.balance?.breakdown?.map((b) => b.granted_balance).sort((a, b) => (a ?? 0) - (b ?? 0)); + const balances = res.balance?.breakdown + ?.map((b) => b.granted_balance) + .sort((a, b) => (a ?? 0) - (b ?? 0)); expect(balances).toEqual([50, 100]); }); @@ -168,7 +172,11 @@ describe(`${chalk.yellowBright("track-breakdown-mixed-entity: per-entity + entit // Entity-1 breakdown sum should be 70 expect(trackRes.balance?.breakdown).toHaveLength(2); - const entitySum = trackRes.balance?.breakdown?.reduce((sum, b) => sum + (b.current_balance ?? 0), 0) ?? 0; + const entitySum = + trackRes.balance?.breakdown?.reduce( + (sum, b) => sum + (b.current_balance ?? 0), + 0, + ) ?? 0; expect(entitySum).toBe(70); // Customer should reflect deduction @@ -201,4 +209,3 @@ describe(`${chalk.yellowBright("track-breakdown-mixed-entity: per-entity + entit }); }); }); - diff --git a/server/tests/balances/track/breakdown/track-breakdown5.test.ts b/server/tests/balances/track/breakdown/track-breakdown5.test.ts index 4b90bb6e7..56d5350d3 100644 --- a/server/tests/balances/track/breakdown/track-breakdown5.test.ts +++ b/server/tests/balances/track/breakdown/track-breakdown5.test.ts @@ -298,4 +298,3 @@ describe(`${chalk.yellowBright("track-breakdown5: prepaid + pay-per-use monthly }); }); }); - diff --git a/server/tests/balances/track/concurrency/concurrent-track5.test.ts b/server/tests/balances/track/concurrency/concurrent-track5.test.ts index d19a9ce57..4ce887cc1 100644 --- a/server/tests/balances/track/concurrency/concurrent-track5.test.ts +++ b/server/tests/balances/track/concurrency/concurrent-track5.test.ts @@ -201,7 +201,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing per-entity track with concu expect(finalEntityRes.features![TestFeature.Messages].balance).toBe(-100); expect(finalEntityRes.features![TestFeature.Messages].usage).toBe(600); - expect(finalEntityRes.features![TestFeature.Messages].usage_limit).toBe(600); + expect(finalEntityRes.features![TestFeature.Messages].usage_limit).toBe( + 600, + ); // Verify other seats still at 500 in database for (const seatId of ["seat2", "seat3", "seat4", "seat5"]) { diff --git a/server/tests/balances/track/concurrency/concurrent-track6.test.ts b/server/tests/balances/track/concurrency/concurrent-track6.test.ts index d54e01514..200da341f 100644 --- a/server/tests/balances/track/concurrency/concurrent-track6.test.ts +++ b/server/tests/balances/track/concurrency/concurrent-track6.test.ts @@ -1,10 +1,10 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, type LimitedItem } from "@autumn/shared"; -import chalk from "chalk"; -import { Decimal } from "decimal.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import { timeout } from "@tests/utils/genUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { Decimal } from "decimal.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/balances/track/entity-products/track-entity-products3.test.ts b/server/tests/balances/track/entity-products/track-entity-products3.test.ts index 39d9b15c8..78b0f3740 100644 --- a/server/tests/balances/track/entity-products/track-entity-products3.test.ts +++ b/server/tests/balances/track/entity-products/track-entity-products3.test.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, ProductItemFeatureType } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/balances/track/legacy/track-legacy1.test.ts b/server/tests/balances/track/legacy/track-legacy1.test.ts index 280b247f6..e16dd9b24 100644 --- a/server/tests/balances/track/legacy/track-legacy1.test.ts +++ b/server/tests/balances/track/legacy/track-legacy1.test.ts @@ -1,9 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion } from "@autumn/shared"; -import chalk from "chalk"; -import { Decimal } from "decimal.js"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { Decimal } from "decimal.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/balances/track/misc/track-misc1.test.ts b/server/tests/balances/track/misc/track-misc1.test.ts index 949b930b3..cddc44f64 100644 --- a/server/tests/balances/track/misc/track-misc1.test.ts +++ b/server/tests/balances/track/misc/track-misc1.test.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, CusExpand } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; diff --git a/server/tests/balances/track/misc/track-misc2.test.ts b/server/tests/balances/track/misc/track-misc2.test.ts index 415fcb3a2..7594ca6b6 100644 --- a/server/tests/balances/track/misc/track-misc2.test.ts +++ b/server/tests/balances/track/misc/track-misc2.test.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { EventService } from "../../../../src/internal/api/events/EventService.js"; import { timeout } from "../../../utils/genUtils.js"; diff --git a/server/tests/balances/track/misc/track-misc3.test.ts b/server/tests/balances/track/misc/track-misc3.test.ts index ba69fc8fe..5c4cf13d3 100644 --- a/server/tests/balances/track/misc/track-misc3.test.ts +++ b/server/tests/balances/track/misc/track-misc3.test.ts @@ -1,8 +1,8 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { EventService } from "../../../../src/internal/api/events/EventService.js"; import { initCustomerV3 } from "../../../../src/utils/scriptUtils/testUtils/initCustomerV3.js"; diff --git a/server/tests/balances/update/legacy-balance-update1.test.ts b/server/tests/balances/update/legacy-balance-update1.test.ts index 8586317e4..b4df05bf5 100644 --- a/server/tests/balances/update/legacy-balance-update1.test.ts +++ b/server/tests/balances/update/legacy-balance-update1.test.ts @@ -10,65 +10,65 @@ import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js" import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const pro = constructProduct({ - type: "pro", - items: [ - constructFeatureItem({ - featureId: TestFeature.Credits, - includedUsage: 500, - }), - ], + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Credits, + includedUsage: 500, + }), + ], }); const testCase = "legacy-balance-update1"; describe(`${chalk.yellowBright("legacy-balance-update1: allow updating balances for an entity")}`, () => { - const customerId = testCase; - const entityId = `${testCase}-user-1`; - const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); + const customerId = testCase; + const entityId = `${testCase}-user-1`; + const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 }); - beforeAll(async () => { - await initCustomerV3({ - ctx, - customerId, - withTestClock: true, - attachPm: "success", - }); + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + attachPm: "success", + }); - await autumnV1.entities.create(customerId, [ - { - id: entityId, - name: "User 1", - feature_id: TestFeature.Credits, - }, - ]); + await autumnV1.entities.create(customerId, [ + { + id: entityId, + name: "User 1", + feature_id: TestFeature.Credits, + }, + ]); - await initProductsV0({ - ctx, - products: [pro], - prefix: testCase, - }); + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); - await autumnV1.attach({ - customer_id: customerId, - entity_id: entityId, - product_id: pro.id, - }); - }); + await autumnV1.attach({ + customer_id: customerId, + entity_id: entityId, + product_id: pro.id, + }); + }); - test("should allow updating balances for an entity", async () => { - await autumnV1.customers.setBalance({ - customerId: customerId, - entityId: entityId, - balances: [ - { - feature_id: TestFeature.Credits, - balance: 100, - }, - ], - }); + test("should allow updating balances for an entity", async () => { + await autumnV1.customers.setBalance({ + customerId: customerId, + entityId: entityId, + balances: [ + { + feature_id: TestFeature.Credits, + balance: 100, + }, + ], + }); - const entity = await autumnV1.entities.get(customerId, entityId); + const entity = await autumnV1.entities.get(customerId, entityId); - expect(entity.features.credits.balance).toBe(100); - }); + expect(entity.features.credits.balance).toBe(100); + }); }); diff --git a/server/tests/balances/update/update-combined/update-combined3.test.ts b/server/tests/balances/update/update-combined/update-combined3.test.ts index c752eba52..a2bcdc013 100644 --- a/server/tests/balances/update/update-combined/update-combined3.test.ts +++ b/server/tests/balances/update/update-combined/update-combined3.test.ts @@ -168,4 +168,3 @@ describe(`${chalk.yellowBright("update-combined3: current_balance + granted_bala }); }); }); - diff --git a/server/tests/balances/update/update-current-balance/basic/update-current-balance2.test.ts b/server/tests/balances/update/update-current-balance/basic/update-current-balance2.test.ts index bdbdf225f..7eb1ed384 100644 --- a/server/tests/balances/update/update-current-balance/basic/update-current-balance2.test.ts +++ b/server/tests/balances/update/update-current-balance/basic/update-current-balance2.test.ts @@ -135,4 +135,3 @@ describe(`${chalk.yellowBright("update-current-balance2: update balance after tr }); }); }); - diff --git a/server/tests/balances/update/update-current-balance/basic/update-current-balance5.test.ts b/server/tests/balances/update/update-current-balance/basic/update-current-balance5.test.ts index 36216249c..19f5add31 100644 --- a/server/tests/balances/update/update-current-balance/basic/update-current-balance5.test.ts +++ b/server/tests/balances/update/update-current-balance/basic/update-current-balance5.test.ts @@ -96,8 +96,13 @@ describe(`${chalk.yellowBright("update-current-balance5: update balance with dec // Balance should be 45.30 now (72.65 - 27.35) const beforeUpdate = await autumnV2.customers.get(customerId); - expect(beforeUpdate.balances[TestFeature.Credits].current_balance).toBeCloseTo(45.30, 2); - expect(beforeUpdate.balances[TestFeature.Credits].usage).toBeCloseTo(27.35, 2); + expect( + beforeUpdate.balances[TestFeature.Credits].current_balance, + ).toBeCloseTo(45.3, 2); + expect(beforeUpdate.balances[TestFeature.Credits].usage).toBeCloseTo( + 27.35, + 2, + ); // Update to 50.50 await autumnV2.balances.update({ @@ -153,4 +158,3 @@ describe(`${chalk.yellowBright("update-current-balance5: update balance with dec expect(balanceFromDb.current_balance).toBeCloseTo(0.01, 2); }); }); - diff --git a/server/tests/external-psps/revenuecat/revenuecatWebhooks.test.ts b/server/tests/external-psps/revenuecat/revenuecatWebhooks.test.ts index 51fda2298..0f036f7eb 100644 --- a/server/tests/external-psps/revenuecat/revenuecatWebhooks.test.ts +++ b/server/tests/external-psps/revenuecat/revenuecatWebhooks.test.ts @@ -1,9 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { - ApiVersion, - AppEnv, - CusProductStatus, - customers, + ApiVersion, + AppEnv, + CusProductStatus, + customers, } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; @@ -19,8 +19,8 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; import { - expectWebhookSuccess, - RevenueCatWebhookClient, + expectWebhookSuccess, + RevenueCatWebhookClient, } from "./utils/revenue-cat-webhook-client.js"; const testCase = "rc1"; @@ -33,363 +33,363 @@ const RC_ADD_ON_ID = "com.app.add_on_pack"; // Autumn product definitions const messagesFeature = constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 1000, + featureId: TestFeature.Messages, + includedUsage: 1000, }); const proMonthly = constructProduct({ - id: `${testCase}-pro-monthly`, - type: "pro", - items: [messagesFeature], - isDefault: false, + id: `${testCase}-pro-monthly`, + type: "pro", + items: [messagesFeature], + isDefault: false, }); const proYearly = constructProduct({ - id: `${testCase}-pro-yearly`, - type: "pro", - isAnnual: true, - items: [ - constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 1000, - }), - ], - isDefault: false, + id: `${testCase}-pro-yearly`, + type: "pro", + isAnnual: true, + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 1000, + }), + ], + isDefault: false, }); const addOnPack = constructProduct({ - id: `${testCase}-add-on`, - type: "one_off", - items: [ - constructFeatureItem({ - featureId: TestFeature.Messages, - includedUsage: 100, - }), - ], - isAddOn: true, - isDefault: false, + id: `${testCase}-add-on`, + type: "one_off", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + }), + ], + isAddOn: true, + isDefault: false, }); describe(chalk.yellowBright("rc1: RevenueCat webhook integration"), () => { - const customerId = `${testCase}-customer`; - const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 }); - let proMonthlyCusProductId: string | null = null; - let internalCustomerId: string | null = null; - let rcClient: RevenueCatWebhookClient; + const customerId = `${testCase}-customer`; + const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 }); + let proMonthlyCusProductId: string | null = null; + let internalCustomerId: string | null = null; + let rcClient: RevenueCatWebhookClient; - const fetchLatestActiveCusProductId = async () => { - if (!internalCustomerId) { - throw new Error("internalCustomerId not set"); - } + const fetchLatestActiveCusProductId = async () => { + if (!internalCustomerId) { + throw new Error("internalCustomerId not set"); + } - const cusProducts = await CusProductService.list({ - db: ctx.db, - internalCustomerId, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Scheduled, - ], - }); + const cusProducts = await CusProductService.list({ + db: ctx.db, + internalCustomerId, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Scheduled, + ], + }); - const activeSorted = cusProducts - .filter( - (cp) => - cp.status === CusProductStatus.Active || - cp.status === CusProductStatus.PastDue, - ) - .sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0)); + const activeSorted = cusProducts + .filter( + (cp) => + cp.status === CusProductStatus.Active || + cp.status === CusProductStatus.PastDue, + ) + .sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0)); - expect( - activeSorted.length > 0, - "Expected at least one active cus_product for customer", - ).toBe(true); + expect( + activeSorted.length > 0, + "Expected at least one active cus_product for customer", + ).toBe(true); - // Return the latest active cus_product id for the customer - return activeSorted[activeSorted.length - 1]!.id; - }; + // Return the latest active cus_product id for the customer + return activeSorted[activeSorted.length - 1]!.id; + }; - const fetchLatestCusProductIdAnyStatus = async () => { - if (!internalCustomerId) { - throw new Error("internalCustomerId not set"); - } + const fetchLatestCusProductIdAnyStatus = async () => { + if (!internalCustomerId) { + throw new Error("internalCustomerId not set"); + } - const cusProducts = await CusProductService.list({ - db: ctx.db, - internalCustomerId, - inStatuses: undefined, - }); + const cusProducts = await CusProductService.list({ + db: ctx.db, + internalCustomerId, + inStatuses: undefined, + }); - if (cusProducts.length === 0) { - return null; - } + if (cusProducts.length === 0) { + return null; + } - const sorted = [...cusProducts].sort( - (a, b) => (a.created_at ?? 0) - (b.created_at ?? 0), - ); + const sorted = [...cusProducts].sort( + (a, b) => (a.created_at ?? 0) - (b.created_at ?? 0), + ); - return sorted[sorted.length - 1]!.id; - }; + return sorted[sorted.length - 1]!.id; + }; - const getBaselineCusProductId = () => { - if (!proMonthlyCusProductId) { - throw new Error( - "Baseline CusProduct ID was not set from initial purchase", - ); - } + const getBaselineCusProductId = () => { + if (!proMonthlyCusProductId) { + throw new Error( + "Baseline CusProduct ID was not set from initial purchase", + ); + } - return proMonthlyCusProductId; - }; - const updateBaselineCusProductId = (cusProductId: string) => { - proMonthlyCusProductId = cusProductId; - }; + return proMonthlyCusProductId; + }; + const updateBaselineCusProductId = (cusProductId: string) => { + proMonthlyCusProductId = cusProductId; + }; - beforeAll(async () => { - // 1. Configure org with RevenueCat processor config - if ( - ctx.org.processor_configs?.revenuecat?.sandbox_webhook_secret !== - RC_WEBHOOK_SECRET - ) { - await OrgService.update({ - db: ctx.db, - orgId: ctx.org.id, - updates: { - processor_configs: { - ...ctx.org.processor_configs, - revenuecat: { - api_key: encryptData("mock_rc_api_key_live"), - sandbox_api_key: encryptData("mock_rc_api_key_sandbox"), - project_id: "mock_project_live", - sandbox_project_id: "mock_project_sandbox", - webhook_secret: RC_WEBHOOK_SECRET, - sandbox_webhook_secret: RC_WEBHOOK_SECRET, - }, - }, - }, - }); - } + beforeAll(async () => { + // 1. Configure org with RevenueCat processor config + if ( + ctx.org.processor_configs?.revenuecat?.sandbox_webhook_secret !== + RC_WEBHOOK_SECRET + ) { + await OrgService.update({ + db: ctx.db, + orgId: ctx.org.id, + updates: { + processor_configs: { + ...ctx.org.processor_configs, + revenuecat: { + api_key: encryptData("mock_rc_api_key_live"), + sandbox_api_key: encryptData("mock_rc_api_key_sandbox"), + project_id: "mock_project_live", + sandbox_project_id: "mock_project_sandbox", + webhook_secret: RC_WEBHOOK_SECRET, + sandbox_webhook_secret: RC_WEBHOOK_SECRET, + }, + }, + }, + }); + } - // Initialize RevenueCat webhook client - rcClient = new RevenueCatWebhookClient({ - orgId: ctx.org.id, - env: ctx.env, - webhookSecret: RC_WEBHOOK_SECRET, - }); + // Initialize RevenueCat webhook client + rcClient = new RevenueCatWebhookClient({ + orgId: ctx.org.id, + env: ctx.env, + webhookSecret: RC_WEBHOOK_SECRET, + }); - // 2-4. Create products, mappings, and customer concurrently - await Promise.all([ - initProductsV0({ - ctx, - products: [proMonthly, proYearly, addOnPack], - prefix: testCase, - }), - RCMappingService.upsert({ - db: ctx.db, - data: { - org_id: ctx.org.id, - env: AppEnv.Sandbox, - autumn_product_id: proMonthly.id, - revenuecat_product_ids: [RC_PRO_MONTHLY_ID], - }, - }), - RCMappingService.upsert({ - db: ctx.db, - data: { - org_id: ctx.org.id, - env: AppEnv.Sandbox, - autumn_product_id: addOnPack.id, - revenuecat_product_ids: [RC_ADD_ON_ID], - }, - }), - RCMappingService.upsert({ - db: ctx.db, - data: { - org_id: ctx.org.id, - env: AppEnv.Sandbox, - autumn_product_id: proYearly.id, - revenuecat_product_ids: [RC_PRO_YEARLY_ID], - }, - }), - initCustomerV3({ - ctx, - customerId, - withTestClock: false, - }), - ]); + // 2-4. Create products, mappings, and customer concurrently + await Promise.all([ + initProductsV0({ + ctx, + products: [proMonthly, proYearly, addOnPack], + prefix: testCase, + }), + RCMappingService.upsert({ + db: ctx.db, + data: { + org_id: ctx.org.id, + env: AppEnv.Sandbox, + autumn_product_id: proMonthly.id, + revenuecat_product_ids: [RC_PRO_MONTHLY_ID], + }, + }), + RCMappingService.upsert({ + db: ctx.db, + data: { + org_id: ctx.org.id, + env: AppEnv.Sandbox, + autumn_product_id: addOnPack.id, + revenuecat_product_ids: [RC_ADD_ON_ID], + }, + }), + RCMappingService.upsert({ + db: ctx.db, + data: { + org_id: ctx.org.id, + env: AppEnv.Sandbox, + autumn_product_id: proYearly.id, + revenuecat_product_ids: [RC_PRO_YEARLY_ID], + }, + }), + initCustomerV3({ + ctx, + customerId, + withTestClock: false, + }), + ]); - const dbCustomer = await ctx.db.query.customers.findFirst({ - where: eq(customers.id, customerId), - }); - expect(dbCustomer).toBeDefined(); - internalCustomerId = dbCustomer!.internal_id; - }); + const dbCustomer = await ctx.db.query.customers.findFirst({ + where: eq(customers.id, customerId), + }); + expect(dbCustomer).toBeDefined(); + internalCustomerId = dbCustomer!.internal_id; + }); - test("should create customer with pro monthly product", async () => { - const result = await rcClient.initialPurchase({ - productId: RC_PRO_MONTHLY_ID, - appUserId: customerId, - originalTransactionId: "1234567890", - }); - expectWebhookSuccess(result); + test("should create customer with pro monthly product", async () => { + const result = await rcClient.initialPurchase({ + productId: RC_PRO_MONTHLY_ID, + appUserId: customerId, + originalTransactionId: "1234567890", + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(proMonthly.id); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(proMonthly.id); - proMonthlyCusProductId = await fetchLatestActiveCusProductId(); - }); + proMonthlyCusProductId = await fetchLatestActiveCusProductId(); + }); - test("should upgrade customer to pro yearly product upon renewal", async () => { - const result = await rcClient.renewal({ - productId: RC_PRO_YEARLY_ID, - appUserId: customerId, - originalTransactionId: "1234567890", - }); - expectWebhookSuccess(result); + test("should upgrade customer to pro yearly product upon renewal", async () => { + const result = await rcClient.renewal({ + productId: RC_PRO_YEARLY_ID, + appUserId: customerId, + originalTransactionId: "1234567890", + }); + expectWebhookSuccess(result); - await fetchLatestActiveCusProductId(); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(proYearly.id); - }); + await fetchLatestActiveCusProductId(); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(proYearly.id); + }); - test("should downgrade customer to pro monthly product upon initial purchase", async () => { - const result = await rcClient.initialPurchase({ - productId: RC_PRO_MONTHLY_ID, - appUserId: customerId, - originalTransactionId: "1234567890", - }); - expectWebhookSuccess(result); + test("should downgrade customer to pro monthly product upon initial purchase", async () => { + const result = await rcClient.initialPurchase({ + productId: RC_PRO_MONTHLY_ID, + appUserId: customerId, + originalTransactionId: "1234567890", + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(proMonthly.id); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(proMonthly.id); - const currentCusProductId = await fetchLatestActiveCusProductId(); - console.log("currentCusProductId", currentCusProductId); - expect(currentCusProductId).not.toBe(getBaselineCusProductId()); - updateBaselineCusProductId(currentCusProductId); - }); + const currentCusProductId = await fetchLatestActiveCusProductId(); + console.log("currentCusProductId", currentCusProductId); + expect(currentCusProductId).not.toBe(getBaselineCusProductId()); + updateBaselineCusProductId(currentCusProductId); + }); - test("should go to cancelling state upon cancellation", async () => { - const result = await rcClient.cancellation({ - productId: RC_PRO_MONTHLY_ID, - appUserId: customerId, - originalTransactionId: "1234567890", - expirationAtMs: Date.now() + 1000 * 60 * 60 * 24 * 30, - }); - expectWebhookSuccess(result); + test("should go to cancelling state upon cancellation", async () => { + const result = await rcClient.cancellation({ + productId: RC_PRO_MONTHLY_ID, + appUserId: customerId, + originalTransactionId: "1234567890", + expirationAtMs: Date.now() + 1000 * 60 * 60 * 24 * 30, + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(proMonthly.id); - const canceledAt = customer.products[0].canceled_at ?? 0; - expect(typeof canceledAt).toBe("number"); - expect(Math.abs(Date.now() - canceledAt)).toBeLessThanOrEqual(3000); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(proMonthly.id); + const canceledAt = customer.products[0].canceled_at ?? 0; + expect(typeof canceledAt).toBe("number"); + expect(Math.abs(Date.now() - canceledAt)).toBeLessThanOrEqual(3000); - const currentCusProductId = await fetchLatestActiveCusProductId(); - expect(currentCusProductId).toBe(getBaselineCusProductId()); - }); + const currentCusProductId = await fetchLatestActiveCusProductId(); + expect(currentCusProductId).toBe(getBaselineCusProductId()); + }); - test("should uncancel customer after cancellation event", async () => { - const result = await rcClient.uncancellation({ - productId: RC_PRO_MONTHLY_ID, - appUserId: customerId, - }); - expectWebhookSuccess(result); + test("should uncancel customer after cancellation event", async () => { + const result = await rcClient.uncancellation({ + productId: RC_PRO_MONTHLY_ID, + appUserId: customerId, + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(proMonthly.id); - expect(customer.products[0].canceled_at).toBeNull(); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(proMonthly.id); + expect(customer.products[0].canceled_at).toBeNull(); - const currentCusProductId = await fetchLatestActiveCusProductId(); - expect(currentCusProductId).toBe(getBaselineCusProductId()); - }); + const currentCusProductId = await fetchLatestActiveCusProductId(); + expect(currentCusProductId).toBe(getBaselineCusProductId()); + }); - test("should mark product as past due upon billing issue", async () => { - const result = await rcClient.billingIssue({ - productId: RC_PRO_MONTHLY_ID, - appUserId: customerId, - originalTransactionId: "1234567890", - }); - expectWebhookSuccess(result); + test("should mark product as past due upon billing issue", async () => { + const result = await rcClient.billingIssue({ + productId: RC_PRO_MONTHLY_ID, + appUserId: customerId, + originalTransactionId: "1234567890", + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(proMonthly.id); - expect(String(customer.products[0].status)).toBe("past_due"); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(proMonthly.id); + expect(String(customer.products[0].status)).toBe("past_due"); - const currentCusProductId = await fetchLatestActiveCusProductId(); - expect(currentCusProductId).toBe(getBaselineCusProductId()); - }); + const currentCusProductId = await fetchLatestActiveCusProductId(); + expect(currentCusProductId).toBe(getBaselineCusProductId()); + }); - test("should go to expired state upon expiration", async () => { - const result = await rcClient.expiration({ - productId: RC_PRO_MONTHLY_ID, - appUserId: customerId, - originalTransactionId: "1234567890", - }); - expectWebhookSuccess(result); + test("should go to expired state upon expiration", async () => { + const result = await rcClient.expiration({ + productId: RC_PRO_MONTHLY_ID, + appUserId: customerId, + originalTransactionId: "1234567890", + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(0); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(0); - const latestCusProductId = await fetchLatestCusProductIdAnyStatus(); - // After expiration, there may no longer be a cus_product row at all. In that - // case, we just assert there are no cus_products for this customer anymore. - if (latestCusProductId === null) { - const allCusProducts = await CusProductService.list({ - db: ctx.db, - internalCustomerId: internalCustomerId!, - inStatuses: undefined, - }); - expect(allCusProducts.length).toBe(0); - } else { - expect(latestCusProductId).toBe(getBaselineCusProductId()); - } - }); + const latestCusProductId = await fetchLatestCusProductIdAnyStatus(); + // After expiration, there may no longer be a cus_product row at all. In that + // case, we just assert there are no cus_products for this customer anymore. + if (latestCusProductId === null) { + const allCusProducts = await CusProductService.list({ + db: ctx.db, + internalCustomerId: internalCustomerId!, + inStatuses: undefined, + }); + expect(allCusProducts.length).toBe(0); + } else { + expect(latestCusProductId).toBe(getBaselineCusProductId()); + } + }); - test("should attach add-on product after expiration via non-renewing purchase", async () => { - const result = await rcClient.nonRenewingPurchase({ - productId: RC_ADD_ON_ID, - appUserId: customerId, - originalTransactionId: "add_on_tx_12345", - }); - expectWebhookSuccess(result); + test("should attach add-on product after expiration via non-renewing purchase", async () => { + const result = await rcClient.nonRenewingPurchase({ + productId: RC_ADD_ON_ID, + appUserId: customerId, + originalTransactionId: "add_on_tx_12345", + }); + expectWebhookSuccess(result); - const customer = await autumnV1.customers.get(customerId); - expect(customer).toBeDefined(); - expect(customer.id).toBe(customerId); - expect(customer.products).toHaveLength(1); - expect(customer.products[0].id).toBe(addOnPack.id); + const customer = await autumnV1.customers.get(customerId); + expect(customer).toBeDefined(); + expect(customer.id).toBe(customerId); + expect(customer.products).toHaveLength(1); + expect(customer.products[0].id).toBe(addOnPack.id); - const addOnCusProducts = await CusProductService.getByProductId({ - db: ctx.db, - productId: addOnPack.id, - orgId: ctx.org.id, - env: ctx.env, - limit: 1, - }); - expect( - addOnCusProducts.length > 0, - `CusProduct for add-on product ${addOnPack.id} should exist`, - ).toBe(true); - const addOnCusProductId = addOnCusProducts[0]!.id; - expect(typeof addOnCusProductId).toBe("string"); - }); + const addOnCusProducts = await CusProductService.getByProductId({ + db: ctx.db, + productId: addOnPack.id, + orgId: ctx.org.id, + env: ctx.env, + limit: 1, + }); + expect( + addOnCusProducts.length > 0, + `CusProduct for add-on product ${addOnPack.id} should exist`, + ).toBe(true); + const addOnCusProductId = addOnCusProducts[0]!.id; + expect(typeof addOnCusProductId).toBe("string"); + }); }); diff --git a/server/tests/integration/balances/check/check-public-key.test.ts b/server/tests/integration/balances/check/check-public-key.test.ts index d292abaae..afb499442 100644 --- a/server/tests/integration/balances/check/check-public-key.test.ts +++ b/server/tests/integration/balances/check/check-public-key.test.ts @@ -28,7 +28,11 @@ async function setupPublicKeyScenario({ customerId }: { customerId: string }) { items: [messagesItem], }); - const { customerId: cusId, autumnV1, ctx } = await initScenario({ + const { + customerId: cusId, + autumnV1, + ctx, + } = await initScenario({ customerId, setup: [s.customer({ testClock: false }), s.products({ list: [freeProd] })], actions: [s.attach({ productId: freeProd.id })], @@ -63,120 +67,111 @@ async function setupPublicKeyScenario({ customerId }: { customerId: string }) { // CHECK: Public key works for /check endpoint // ═══════════════════════════════════════════════════════════════════ -test.concurrent( - `${chalk.yellowBright("check-public-key: /check works with public key")}`, - async () => { - const { customerId, autumnPublic } = await setupPublicKeyScenario({ - customerId: "check-public-key", - }); +test.concurrent(`${chalk.yellowBright("check-public-key: /check works with public key")}`, async () => { + const { customerId, autumnPublic } = await setupPublicKeyScenario({ + customerId: "check-public-key", + }); - const checkRes = await autumnPublic.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - required_balance: 100, - }); + const checkRes = await autumnPublic.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + required_balance: 100, + }); - expect(checkRes).toMatchObject({ - allowed: true, - customer_id: customerId, - feature_id: TestFeature.Messages, - balance: 1000, - required_balance: 100, - code: SuccessCode.FeatureFound, - usage: 0, - included_usage: 1000, - overage_allowed: false, - }); - expect(checkRes.next_reset_at).toBeDefined(); - }, -); + expect(checkRes).toMatchObject({ + allowed: true, + customer_id: customerId, + feature_id: TestFeature.Messages, + balance: 1000, + required_balance: 100, + code: SuccessCode.FeatureFound, + usage: 0, + included_usage: 1000, + overage_allowed: false, + }); + expect(checkRes.next_reset_at).toBeDefined(); +}); // ═══════════════════════════════════════════════════════════════════ // CHECK: send_event blocked with public key // ═══════════════════════════════════════════════════════════════════ -test.concurrent( - `${chalk.yellowBright("check-public-key-send-event-blocked: send_event with public key should error")}`, - async () => { - const { customerId, autumnV1, autumnPublic } = await setupPublicKeyScenario( - { customerId: "check-public-key-send-event-blocked" }, - ); +test.concurrent(`${chalk.yellowBright("check-public-key-send-event-blocked: send_event with public key should error")}`, async () => { + const { customerId, autumnV1, autumnPublic } = await setupPublicKeyScenario({ + customerId: "check-public-key-send-event-blocked", + }); - const customerBefore = - await autumnV1.customers.get(customerId); - const balanceBefore = customerBefore.features[TestFeature.Messages].balance; - const usageBefore = customerBefore.features[TestFeature.Messages].usage; + const customerBefore = + await autumnV1.customers.get(customerId); + const balanceBefore = customerBefore.features[TestFeature.Messages].balance; + const usageBefore = customerBefore.features[TestFeature.Messages].usage; - await expectAutumnError({ - func: async () => { - await autumnPublic.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - required_balance: 50, - send_event: true, - }); - }, - }); + await expectAutumnError({ + func: async () => { + await autumnPublic.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + required_balance: 50, + send_event: true, + }); + }, + }); - const customerAfter = - await autumnV1.customers.get(customerId); + const customerAfter = await autumnV1.customers.get(customerId); - expect(customerAfter.features[TestFeature.Messages].balance).toBe( - balanceBefore, - ); - expect(customerAfter.features[TestFeature.Messages].usage).toBe( - usageBefore, - ); - }, -); + expect(customerAfter.features[TestFeature.Messages].balance).toBe( + balanceBefore, + ); + expect(customerAfter.features[TestFeature.Messages].usage).toBe(usageBefore); +}); // ═══════════════════════════════════════════════════════════════════ // CHECK: send_event works with secret key // ═══════════════════════════════════════════════════════════════════ -test.concurrent( - `${chalk.yellowBright("check-send-event: send_event with secret key tracks usage")}`, - async () => { - const { customerId, autumnV1 } = await setupPublicKeyScenario({ - customerId: "check-send-event", - }); +test.concurrent(`${chalk.yellowBright("check-send-event: send_event with secret key tracks usage")}`, async () => { + const { customerId, autumnV1 } = await setupPublicKeyScenario({ + customerId: "check-send-event", + }); - // Should track usage when send_event: true with secret key - const checkRes = await autumnV1.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - required_balance: 150, - send_event: true, - }); + // Should track usage when send_event: true with secret key + const checkRes = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + required_balance: 150, + send_event: true, + }); - expect(checkRes.allowed).toBe(true); - expect(checkRes.balance).toBe(1000 - 150); + expect(checkRes.allowed).toBe(true); + expect(checkRes.balance).toBe(1000 - 150); - await timeout(2000); + await timeout(2000); - const customerAfter = - await autumnV1.customers.get(customerId); + const customerAfter = await autumnV1.customers.get(customerId); - expect(customerAfter.features[TestFeature.Messages].balance).toBe(850); - expect(customerAfter.features[TestFeature.Messages].usage).toBe(150); + expect(customerAfter.features[TestFeature.Messages].balance).toBe(850); + expect(customerAfter.features[TestFeature.Messages].usage).toBe(150); - // Should NOT track when allowed: false (insufficient balance) - const checkResInsufficient = await autumnV1.check({ - customer_id: customerId, - feature_id: TestFeature.Messages, - required_balance: 900, // More than available (850) - send_event: true, - }); + // Should NOT track when allowed: false (insufficient balance) + const checkResInsufficient = await autumnV1.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + required_balance: 900, // More than available (850) + send_event: true, + }); - expect(checkResInsufficient.allowed).toBe(false); + expect(checkResInsufficient.allowed).toBe(false); - await timeout(2000); + await timeout(2000); - const customerAfterInsufficient = - await autumnV1.customers.get(customerId); + const customerAfterInsufficient = + await autumnV1.customers.get(customerId); - // Balance and usage should remain unchanged - expect(customerAfterInsufficient.features[TestFeature.Messages].balance).toBe(850); - expect(customerAfterInsufficient.features[TestFeature.Messages].usage).toBe(150); - }, -); + // Balance and usage should remain unchanged + expect(customerAfterInsufficient.features[TestFeature.Messages].balance).toBe( + 850, + ); + expect(customerAfterInsufficient.features[TestFeature.Messages].usage).toBe( + 150, + ); +}); diff --git a/server/tests/integration/billing/cancel/add-ons/cancel-addon1.test.ts b/server/tests/integration/billing/cancel/add-ons/cancel-addon1.test.ts index 5e187b120..bb8dca02d 100644 --- a/server/tests/integration/billing/cancel/add-ons/cancel-addon1.test.ts +++ b/server/tests/integration/billing/cancel/add-ons/cancel-addon1.test.ts @@ -1,6 +1,8 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, CusProductStatus } from "@autumn/shared"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; import { TestFeature } from "@tests/setup/v2Features.js"; +import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; @@ -8,8 +10,6 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; -import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached"; const pro = constructProduct({ type: "pro", diff --git a/server/tests/integration/billing/cancel/add-ons/cancel-addon4.test.ts b/server/tests/integration/billing/cancel/add-ons/cancel-addon4.test.ts index 2d825191c..2044255b7 100644 --- a/server/tests/integration/billing/cancel/add-ons/cancel-addon4.test.ts +++ b/server/tests/integration/billing/cancel/add-ons/cancel-addon4.test.ts @@ -1,9 +1,17 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, CusProductStatus } from "@autumn/shared"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; import { TestFeature } from "@tests/setup/v2Features.js"; +import { + expectProductAttached, + expectProductNotAttached, +} from "@tests/utils/expectUtils/expectProductAttached"; +import { timeout } from "@tests/utils/genUtils"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils"; +import { CusService } from "@/internal/customers/CusService"; import { constructArrearItem, constructFeatureItem, @@ -11,14 +19,6 @@ import { import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils"; -import { CusService } from "@/internal/customers/CusService"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; -import { - expectProductAttached, - expectProductNotAttached, -} from "@tests/utils/expectUtils/expectProductAttached"; -import { timeout } from "@tests/utils/genUtils"; const pro = constructProduct({ type: "pro", diff --git a/server/tests/integration/billing/invoice-action-required/invoice-action-required2.test.ts b/server/tests/integration/billing/invoice-action-required/invoice-action-required2.test.ts index f6587d62a..549dfb054 100644 --- a/server/tests/integration/billing/invoice-action-required/invoice-action-required2.test.ts +++ b/server/tests/integration/billing/invoice-action-required/invoice-action-required2.test.ts @@ -1,17 +1,17 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; +import { timeout } from "@tests/utils/genUtils"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; +import { handleVoidInvoiceCron } from "@/cron/invoiceCron/runInvoiceCron"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { attachAuthenticatePaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { MetadataService } from "@/internal/metadata/MetadataService"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { handleVoidInvoiceCron } from "@/cron/invoiceCron/runInvoiceCron"; -import { MetadataService } from "@/internal/metadata/MetadataService"; -import { timeout } from "@tests/utils/genUtils"; // UNCOMMENT FROM HERE const pro = constructProduct({ diff --git a/server/tests/integration/billing/invoice-action-required/invoice-action-required3.test.ts b/server/tests/integration/billing/invoice-action-required/invoice-action-required3.test.ts index 3f64b26a2..b0b7d209b 100644 --- a/server/tests/integration/billing/invoice-action-required/invoice-action-required3.test.ts +++ b/server/tests/integration/billing/invoice-action-required/invoice-action-required3.test.ts @@ -5,6 +5,7 @@ import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import { expectSubItemsCorrect } from "@tests/utils/expectUtils/expectSubUtils.js"; +import { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import type Stripe from "stripe"; @@ -21,8 +22,6 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout"; - const testCase = "invoice-action-required3"; export const pro = constructProduct({ diff --git a/server/tests/integration/billing/invoice-action-required/invoice-action-required4.test.ts b/server/tests/integration/billing/invoice-action-required/invoice-action-required4.test.ts index 761b0a43e..361d776e7 100644 --- a/server/tests/integration/billing/invoice-action-required/invoice-action-required4.test.ts +++ b/server/tests/integration/billing/invoice-action-required/invoice-action-required4.test.ts @@ -5,7 +5,13 @@ import { ProductItemInterval, RolloverExpiryDurationType, } from "@autumn/shared"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; import { TestFeature } from "@tests/setup/v2Features.js"; +import { + expectProductAttached, + expectProductNotAttached, +} from "@tests/utils/expectUtils/expectProductAttached"; +import { completeInvoiceConfirmation } from "@tests/utils/stripeUtils/completeInvoiceConfirmation"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; @@ -18,12 +24,6 @@ import { } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; -import { - expectProductAttached, - expectProductNotAttached, -} from "@tests/utils/expectUtils/expectProductAttached"; -import { completeInvoiceConfirmation } from "@tests/utils/stripeUtils/completeInvoiceConfirmation"; const pro = constructProduct({ type: "pro", diff --git a/server/tests/integration/billing/invoice-action-required/new-subscription/new-subscription-action-required1.test.ts b/server/tests/integration/billing/invoice-action-required/new-subscription/new-subscription-action-required1.test.ts index c9c75e66f..1ed19a167 100644 --- a/server/tests/integration/billing/invoice-action-required/new-subscription/new-subscription-action-required1.test.ts +++ b/server/tests/integration/billing/invoice-action-required/new-subscription/new-subscription-action-required1.test.ts @@ -1,5 +1,6 @@ import { beforeAll, describe, expect, it } from "bun:test"; import { ApiVersion, SuccessCode } from "@autumn/shared"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached"; import { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout"; @@ -10,7 +11,6 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; const pro = constructProduct({ type: "pro", diff --git a/server/tests/integration/billing/new-billing-subscription/new-billing-subscription1.test.ts b/server/tests/integration/billing/new-billing-subscription/new-billing-subscription1.test.ts index a4a0cee7b..f64a8fc9c 100644 --- a/server/tests/integration/billing/new-billing-subscription/new-billing-subscription1.test.ts +++ b/server/tests/integration/billing/new-billing-subscription/new-billing-subscription1.test.ts @@ -7,6 +7,7 @@ import { getCusStripeSubCount, } from "@autumn/shared"; import { TestFeature } from "@tests/setup/v2Features.js"; +import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; @@ -21,7 +22,6 @@ import { } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached"; const paidAddOn = constructRawProduct({ id: "addOn", diff --git a/server/tests/integration/billing/renew/renew-addon1.test.ts b/server/tests/integration/billing/renew/renew-addon1.test.ts index f3daf0a4c..ce91411f7 100644 --- a/server/tests/integration/billing/renew/renew-addon1.test.ts +++ b/server/tests/integration/billing/renew/renew-addon1.test.ts @@ -1,6 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ApiVersion, CusProductStatus } from "@autumn/shared"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js"; import { TestFeature } from "@tests/setup/v2Features.js"; +import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; +import { timeout } from "@tests/utils/genUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import chalk from "chalk"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; @@ -8,9 +11,6 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js"; -import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; -import { timeout } from "@tests/utils/genUtils.js"; const pro = constructProduct({ type: "pro", diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts index 6b44a48c3..6613b1ec0 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial-entities.test.ts @@ -7,6 +7,7 @@ import { expect, test } from "bun:test"; import { type ApiCustomerV3, ms } from "@autumn/shared"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; import { expectProductActive, expectProductCanceling, @@ -21,10 +22,115 @@ import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect" import { TestFeature } from "@tests/setup/v2Features"; import { items } from "@tests/utils/fixtures/items"; import { products } from "@tests/utils/fixtures/products"; +import { advanceTestClock } from "@tests/utils/stripeUtils"; import { advanceToNextInvoice } from "@tests/utils/testAttachUtils/testAttachUtils"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 0: Single entity trial cancel with consumable overage - no charge +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach proTrial to entity 1 + * - Update entity 1 to add consumable messages + * - Track 200 messages (100 overage) + * - Cancel entity 1 at end of cycle + * - Advance past trial end + * + * Expected Result: + * - Entity 1 is removed after trial ends + * - No overage charges (trial usage not billed) + * - No subscription remains + */ +test(`${chalk.yellowBright("cancel trial EOC entities: single entity consumable overage not charged")}`, async () => { + const customerId = "cancel-trial-eoc-single-ent-overage"; + + const consumableItem = items.consumableMessages({ includedUsage: 100 }); + + const proTrial = products.proWithTrial({ + id: "pro-trial", + items: [], + trialDays: 7, + }); + + const { autumnV1, ctx, entities, testClockId } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [proTrial] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [s.attach({ productId: proTrial.id, entityIndex: 0 })], + }); + + const entityId = entities[0].id; + + // Update entity to add consumable messages + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: proTrial.id, + entity_id: entityId, + items: [consumableItem], + }); + + // Track 200 messages (100 included + 100 overage) + await autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: entityId, + value: 200, + }); + + // Cancel entity at end of cycle + await autumnV1.subscriptions.update({ + customer_id: customerId, + entity_id: entityId, + product_id: proTrial.id, + cancel_action: "cancel_end_of_cycle", + }); + + // Verify entity is canceling + const entityAfterCancel = await autumnV1.entities.get(customerId, entityId); + await expectProductCanceling({ + customer: entityAfterCancel, + productId: proTrial.id, + }); + + // Advance past trial end + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfDays: 8, + }); + + // Verify entity is removed + const entityAfterAdvance = await autumnV1.entities.get(customerId, entityId); + await expectProductNotPresent({ + customer: entityAfterAdvance, + productId: proTrial.id, + }); + + // No subscription should exist + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + subCount: 0, + }); + + // No paid invoice - only $0 invoice from update + const customerAfterAdvance = + await autumnV1.customers.get(customerId); + await expectCustomerInvoiceCorrect({ + customer: customerAfterAdvance, + count: 1, + latestTotal: 0, + }); +}); + // ═══════════════════════════════════════════════════════════════════════════════ // TEST 1: Cancel one entity EOC, other still trialing // ═══════════════════════════════════════════════════════════════════════════════ @@ -33,11 +139,12 @@ import chalk from "chalk"; * Scenario: * - Attach proTrial to entity 1 and entity 2 (merged subscription) * - Cancel entity 1 at end of cycle + * - Advance past trial end * * Expected Result: - * - Entity 1's product should be canceling - * - Entity 2's product should still be trialing - * - Subscription should still be trialing (not canceled yet) + * - Entity 1's product should be canceling, then removed after trial ends + * - Entity 2's product should still be trialing, then active after trial ends + * - Invoice after trial ends should only be for 1 entity ($20) */ test(`${chalk.yellowBright("cancel trial EOC entities: cancel one entity, other still trialing")}`, async () => { const customerId = "cancel-trial-eoc-ent-one"; @@ -50,18 +157,19 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel one entity, other trialDays: 7, }); - const { autumnV1, ctx, entities, advancedTo } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [proTrial] }), - s.entities({ count: 2, featureId: TestFeature.Users }), - ], - actions: [ - s.attach({ productId: proTrial.id, entityIndex: 0 }), - s.attach({ productId: proTrial.id, entityIndex: 1 }), - ], - }); + const { autumnV1, ctx, entities, advancedTo, testClockId } = + await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [proTrial] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.attach({ productId: proTrial.id, entityIndex: 0 }), + s.attach({ productId: proTrial.id, entityIndex: 1 }), + ], + }); const entity1Id = entities[0].id; const entity2Id = entities[1].id; @@ -112,6 +220,167 @@ test(`${chalk.yellowBright("cancel trial EOC entities: cancel one entity, other env: ctx.env, shouldBeTrialing: true, }); + + // Advance past trial end + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfDays: 8, + }); + + // Verify entity 1 is removed + const entity1AfterAdvance = await autumnV1.entities.get( + customerId, + entity1Id, + ); + await expectProductNotPresent({ + customer: entity1AfterAdvance, + productId: proTrial.id, + }); + + // Verify entity 2 is active (trial ended) + const entity2AfterAdvance = await autumnV1.entities.get( + customerId, + entity2Id, + ); + await expectProductActive({ + customer: entity2AfterAdvance, + productId: proTrial.id, + }); + + // Invoice should only be for 1 entity ($20) + const customerAfterAdvance = + await autumnV1.customers.get(customerId); + await expectCustomerInvoiceCorrect({ + customer: customerAfterAdvance, + count: 1, + latestTotal: 20, + latestInvoiceProductId: proTrial.id, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1b: Cancel entity with consumable overage during trial - no overage charge +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach proTrial to entity 1 and entity 2 + * - Update entity 1 to add consumable messages + * - Track 200 messages on entity 1 (100 overage) + * - Cancel entity 1 at end of cycle + * - Advance past trial end + * + * Expected Result: + * - Entity 1's overage during trial should NOT be charged + * - Invoice should only include entity 2's base price ($20) + */ +test(`${chalk.yellowBright("cancel trial EOC entities: consumable overage during trial not charged")}`, async () => { + const customerId = "cancel-trial-eoc-ent-overage"; + + const consumableItem = items.consumableMessages({ includedUsage: 100 }); + + const proTrial = products.proWithTrial({ + id: "pro-trial", + items: [], + trialDays: 7, + }); + + const { autumnV1, ctx, entities, advancedTo, testClockId } = + await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [proTrial] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.attach({ productId: proTrial.id, entityIndex: 0 }), + s.attach({ productId: proTrial.id, entityIndex: 1 }), + ], + }); + + const entity1Id = entities[0].id; + const entity2Id = entities[1].id; + + // Update entity 1 to add consumable messages + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: proTrial.id, + entity_id: entity1Id, + items: [consumableItem], + }); + + // Track 200 messages on entity 1 (100 included + 100 overage) + await autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: entity1Id, + value: 200, + }); + + // Cancel entity 1 at end of cycle + await autumnV1.subscriptions.update({ + customer_id: customerId, + entity_id: entity1Id, + product_id: proTrial.id, + cancel_action: "cancel_end_of_cycle", + }); + + // Verify entity 1 is canceling + const entity1AfterCancel = await autumnV1.entities.get(customerId, entity1Id); + await expectProductCanceling({ + customer: entity1AfterCancel, + productId: proTrial.id, + }); + + // Verify entity 2 is still trialing + const entity2AfterCancel = await autumnV1.entities.get(customerId, entity2Id); + await expectProductTrialing({ + customer: entity2AfterCancel, + productId: proTrial.id, + trialEndsAt: advancedTo + ms.days(7), + }); + + // Advance past trial end + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfDays: 8, + }); + + // Verify entity 1 is removed + const entity1AfterAdvance = await autumnV1.entities.get( + customerId, + entity1Id, + ); + await expectProductNotPresent({ + customer: entity1AfterAdvance, + productId: proTrial.id, + }); + + // Verify entity 2 is active + const entity2AfterAdvance = await autumnV1.entities.get( + customerId, + entity2Id, + ); + await expectProductActive({ + customer: entity2AfterAdvance, + productId: proTrial.id, + }); + + // Invoice should NOT include entity 1's overage charges + // Only entity 2's base price ($20) + const customerAfterAdvance = + await autumnV1.customers.get(customerId); + + // First two attaches create $0 invoice, third invoice is $0 from update, fourth is $20 for entity 2 + await expectCustomerInvoiceCorrect({ + customer: customerAfterAdvance, + count: 4, + latestTotal: 20, // Only entity 2's base price, NO overage from entity 1 + latestInvoiceProductId: proTrial.id, + }); }); // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts index 17f851730..96d66bdd1 100644 --- a/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts +++ b/server/tests/integration/billing/update-subscription/cancel/end-of-cycle/cancel-end-of-cycle-trial.test.ts @@ -431,6 +431,13 @@ test(`${chalk.yellowBright("cancel trial EOC: with free default, free scheduled" scheduled: [free.id], }); + // Verify free is scheduled to start when trial ends (7 days), not a month from now + await expectProductScheduled({ + customer: customerAfterCancel, + productId: free.id, + startsAt: Date.now() + ms.days(7), + }); + // Advance past trial end await advanceToNextInvoice({ stripeCli: ctx.stripeCli, @@ -448,8 +455,6 @@ test(`${chalk.yellowBright("cancel trial EOC: with free default, free scheduled" }); // No paid invoice should be created - const invoices = customerAfterAdvance.invoices ?? []; - for (const invoice of invoices) { - expect(invoice.total).toBe(0); - } + for (const inv of customerAfterAdvance.invoices ?? []) + expect(inv.total).toBe(0); }); diff --git a/server/tests/integration/billing/update-subscription/discounts/amount-off-discount.test.ts b/server/tests/integration/billing/update-subscription/discounts/amount-off-discount.test.ts index c28fcb163..7baf580c5 100644 --- a/server/tests/integration/billing/update-subscription/discounts/amount-off-discount.test.ts +++ b/server/tests/integration/billing/update-subscription/discounts/amount-off-discount.test.ts @@ -13,9 +13,9 @@ import { products } from "@tests/utils/fixtures/products.js"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; import chalk from "chalk"; import { - getStripeSubscription, - createAmountCoupon, applySubscriptionDiscount, + createAmountCoupon, + getStripeSubscription, } from "../../utils/discounts/discountTestUtils.js"; const billingUnits = 12; @@ -25,398 +25,376 @@ const pricePerUnit = 10; // MIGRATED TESTS FROM subscription-discounts.test.ts // ============================================================================= -test.concurrent( - `${chalk.yellowBright("amount-off: $10 off discount applied to upgrade")}`, - async () => { - const customerId = "amt-10-upgrade"; +test.concurrent(`${chalk.yellowBright("amount-off: $10 off discount applied to upgrade")}`, async () => { + const customerId = "amt-10-upgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // $10 off coupon (1000 cents) - const coupon = await createAmountCoupon({ - stripeCli, - amountOffCents: 1000, - }); + // $10 off coupon (1000 cents) + const coupon = await createAmountCoupon({ + stripeCli, + amountOffCents: 1000, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with $10 off: $100 - $10 = $90 - // Total: -$50 + $90 = $40 - const refundAmount = -50; - const discountedCharge = 100 - 10; - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with $10 off: $100 - $10 = $90 + // Total: -$50 + $90 = $40 + const refundAmount = -50; + const discountedCharge = 100 - 10; + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("amount-off: charge capped at zero when discount exceeds charge")}`, - async () => { - const customerId = "amt-cap-zero"; +test.concurrent(`${chalk.yellowBright("amount-off: charge capped at zero when discount exceeds charge")}`, async () => { + const customerId = "amt-cap-zero"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // $100 off coupon (10000 cents) - more than the $50 charge - const coupon = await createAmountCoupon({ - stripeCli, - amountOffCents: 10000, - }); + // $100 off coupon (10000 cents) - more than the $50 charge + const coupon = await createAmountCoupon({ + stripeCli, + amountOffCents: 10000, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Charge is capped at 0 ($50 - $100 = 0, not negative) - // But refund for unused portion still applies: -$50 - // Net = -$50 (refund) + $0 (discounted charge) = -$50 - expect(preview.total).toBe(-50); - }, -); + // Charge is capped at 0 ($50 - $100 = 0, not negative) + // But refund for unused portion still applies: -$50 + // Net = -$50 (refund) + $0 (discounted charge) = -$50 + expect(preview.total).toBe(-50); +}); // ============================================================================= // NEW EDGE CASE TESTS // ============================================================================= -test.concurrent( - `${chalk.yellowBright("amount-off: $5 off small upgrade")}`, - async () => { - const customerId = "amt-5-small"; +test.concurrent(`${chalk.yellowBright("amount-off: $5 off small upgrade")}`, async () => { + const customerId = "amt-5-small"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // $5 off coupon (500 cents) - const coupon = await createAmountCoupon({ - stripeCli, - amountOffCents: 500, - }); + // $5 off coupon (500 cents) + const coupon = await createAmountCoupon({ + stripeCli, + amountOffCents: 500, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with $5 off: $100 - $5 = $95 - // Total: -$50 + $95 = $45 - expect(preview.total).toBe(45); - }, -); + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with $5 off: $100 - $5 = $95 + // Total: -$50 + $95 = $45 + expect(preview.total).toBe(45); +}); -test.concurrent( - `${chalk.yellowBright("amount-off: discount equals charge exactly")}`, - async () => { - const customerId = "amt-exact-match"; +test.concurrent(`${chalk.yellowBright("amount-off: discount equals charge exactly")}`, async () => { + const customerId = "amt-exact-match"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // $50 off coupon (5000 cents) - exactly matches the charge - const coupon = await createAmountCoupon({ - stripeCli, - amountOffCents: 5000, - }); + // $50 off coupon (5000 cents) - exactly matches the charge + const coupon = await createAmountCoupon({ + stripeCli, + amountOffCents: 5000, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with $50 off: $100 - $50 = $50 - // Total: -$50 + $50 = $0 - expect(preview.total).toBe(0); - }, -); + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with $50 off: $100 - $50 = $50 + // Total: -$50 + $50 = $0 + expect(preview.total).toBe(0); +}); -test.concurrent( - `${chalk.yellowBright("amount-off: mixed charges and refunds (only charges get discount)")}`, - async () => { - const customerId = "amt-mixed-charge-refund"; +test.concurrent(`${chalk.yellowBright("amount-off: mixed charges and refunds (only charges get discount)")}`, async () => { + const customerId = "amt-mixed-charge-refund"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - // Start with 10 units = $100 - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + // Start with 10 units = $100 + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // $20 off coupon (2000 cents) - const coupon = await createAmountCoupon({ - stripeCli, - amountOffCents: 2000, - }); + // $20 off coupon (2000 cents) + const coupon = await createAmountCoupon({ + stripeCli, + amountOffCents: 2000, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - // Preview change: decrease to 5 units - // This should create a refund for unused, not a charge - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }); + // Preview change: decrease to 5 units + // This should create a refund for unused, not a charge + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits }], + }); - // Downgrade generates: refund (-$100 for 10 units) + charge ($50 for 5 units) - // Discounts only apply to charges, not refunds - // Charge with $20 off: $50 - $20 = $30 - // Total: -$100 + $30 = -$70 - expect(preview.total).toBe(-70); - }, -); + // Downgrade generates: refund (-$100 for 10 units) + charge ($50 for 5 units) + // Discounts only apply to charges, not refunds + // Charge with $20 off: $50 - $20 = $30 + // Total: -$100 + $30 = -$70 + expect(preview.total).toBe(-70); +}); -test.concurrent( - `${chalk.yellowBright("amount-off: large amount on pure upgrade")}`, - async () => { - const customerId = "amt-large-pure-upgrade"; +test.concurrent(`${chalk.yellowBright("amount-off: large amount on pure upgrade")}`, async () => { + const customerId = "amt-large-pure-upgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - // Start with 0 units (minimum to have a subscription) - { feature_id: TestFeature.Messages, quantity: 1 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + // Start with 0 units (minimum to have a subscription) + { feature_id: TestFeature.Messages, quantity: 1 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // $25 off coupon (2500 cents) - const coupon = await createAmountCoupon({ - stripeCli, - amountOffCents: 2500, - }); + // $25 off coupon (2500 cents) + const coupon = await createAmountCoupon({ + stripeCli, + amountOffCents: 2500, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - // Preview upgrade from 1 to 6 units (adding 5 units = $50) - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 6 * billingUnits }, - ], - }); + // Preview upgrade from 1 to 6 units (adding 5 units = $50) + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [{ feature_id: TestFeature.Messages, quantity: 6 * billingUnits }], + }); - // Upgrade generates: refund (-$10 for 1 unit) + charge ($60 for 6 units) - // Discounts only apply to charges, not refunds - // Charge with $25 off: $60 - $25 = $35 - // Total: -$10 + $35 = $25 - expect(preview.total).toBe(25); - }, -); + // Upgrade generates: refund (-$10 for 1 unit) + charge ($60 for 6 units) + // Discounts only apply to charges, not refunds + // Charge with $25 off: $60 - $25 = $35 + // Total: -$10 + $35 = $25 + expect(preview.total).toBe(25); +}); diff --git a/server/tests/integration/billing/update-subscription/discounts/discount-applies-to.test.ts b/server/tests/integration/billing/update-subscription/discounts/discount-applies-to.test.ts index 8d5e3c18b..122ecdedb 100644 --- a/server/tests/integration/billing/update-subscription/discounts/discount-applies-to.test.ts +++ b/server/tests/integration/billing/update-subscription/discounts/discount-applies-to.test.ts @@ -14,406 +14,391 @@ import { products } from "@tests/utils/fixtures/products.js"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; import chalk from "chalk"; import { - getStripeSubscription, applySubscriptionDiscount, + getStripeSubscription, } from "../../utils/discounts/discountTestUtils.js"; const billingUnits = 12; const pricePerUnit = 10; -test.concurrent( - `${chalk.yellowBright("applies-to: unrestricted discount applies to all products")}`, - async () => { - const customerId = "applies-unrestricted"; +test.concurrent(`${chalk.yellowBright("applies-to: unrestricted discount applies to all products")}`, async () => { + const customerId = "applies-unrestricted"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // Create coupon without applies_to (applies to all) - const coupon = await stripeCli.coupons.create({ - percent_off: 30, - duration: "forever", - // No applies_to - should apply to everything - }); + // Create coupon without applies_to (applies to all) + const coupon = await stripeCli.coupons.create({ + percent_off: 30, + duration: "forever", + // No applies_to - should apply to everything + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 30% off: $100 * 0.7 = $70 - // Total: -$50 + $70 = $20 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.7); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 30% off: $100 * 0.7 = $70 + // Total: -$50 + $70 = $20 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.7); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("applies-to: discount restricted to matching product")}`, - async () => { - const customerId = "applies-matching"; +test.concurrent(`${chalk.yellowBright("applies-to: discount restricted to matching product")}`, async () => { + const customerId = "applies-matching"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // Get the actual Stripe product ID from the subscription - const subscriptionItems = subscription.items.data; - const stripeProductId = - typeof subscriptionItems[0]?.price?.product === "string" - ? subscriptionItems[0].price.product - : subscriptionItems[0]?.price?.product?.id; + // Get the actual Stripe product ID from the subscription + const subscriptionItems = subscription.items.data; + const stripeProductId = + typeof subscriptionItems[0]?.price?.product === "string" + ? subscriptionItems[0].price.product + : subscriptionItems[0]?.price?.product?.id; - if (!stripeProductId) { - throw new Error("Could not find Stripe product ID"); - } + if (!stripeProductId) { + throw new Error("Could not find Stripe product ID"); + } - // Create coupon that applies to this specific product - const coupon = await stripeCli.coupons.create({ - percent_off: 40, - duration: "forever", - applies_to: { - products: [stripeProductId], - }, - }); + // Create coupon that applies to this specific product + const coupon = await stripeCli.coupons.create({ + percent_off: 40, + duration: "forever", + applies_to: { + products: [stripeProductId], + }, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 40% off: $100 * 0.6 = $60 - // Total: -$50 + $60 = $10 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.6); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 40% off: $100 * 0.6 = $60 + // Total: -$50 + $60 = $10 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.6); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("applies-to: discount restricted to non-matching product")}`, - async () => { - const customerId = "applies-non-matching"; +test.concurrent(`${chalk.yellowBright("applies-to: discount restricted to non-matching product")}`, async () => { + const customerId = "applies-non-matching"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // Create a different Stripe product for the coupon to apply to - const otherProduct = await stripeCli.products.create({ - name: `Other Product ${customerId}`, - }); + // Create a different Stripe product for the coupon to apply to + const otherProduct = await stripeCli.products.create({ + name: `Other Product ${customerId}`, + }); - // Create coupon that applies to a DIFFERENT product - const coupon = await stripeCli.coupons.create({ - percent_off: 50, - duration: "forever", - applies_to: { - products: [otherProduct.id], - }, - }); + // Create coupon that applies to a DIFFERENT product + const coupon = await stripeCli.coupons.create({ + percent_off: 50, + duration: "forever", + applies_to: { + products: [otherProduct.id], + }, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Product does NOT match applies_to, so no discount - // Total: -$50 + $100 = $50 - const refundAmount = -50; - const chargeAmount = 100; - const expectedAmount = refundAmount + chargeAmount; + // Upgrade generates: refund (-$50) + charge ($100) + // Product does NOT match applies_to, so no discount + // Total: -$50 + $100 = $50 + const refundAmount = -50; + const chargeAmount = 100; + const expectedAmount = refundAmount + chargeAmount; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("applies-to: multiple products in restriction list")}`, - async () => { - const customerId = "applies-multiple-products"; +test.concurrent(`${chalk.yellowBright("applies-to: multiple products in restriction list")}`, async () => { + const customerId = "applies-multiple-products"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // Get the actual Stripe product ID - const subscriptionItems = subscription.items.data; - const stripeProductId = - typeof subscriptionItems[0]?.price?.product === "string" - ? subscriptionItems[0].price.product - : subscriptionItems[0]?.price?.product?.id; + // Get the actual Stripe product ID + const subscriptionItems = subscription.items.data; + const stripeProductId = + typeof subscriptionItems[0]?.price?.product === "string" + ? subscriptionItems[0].price.product + : subscriptionItems[0]?.price?.product?.id; - if (!stripeProductId) { - throw new Error("Could not find Stripe product ID"); - } + if (!stripeProductId) { + throw new Error("Could not find Stripe product ID"); + } - // Create another product - const otherProduct = await stripeCli.products.create({ - name: `Other Product ${customerId}`, - }); + // Create another product + const otherProduct = await stripeCli.products.create({ + name: `Other Product ${customerId}`, + }); - // Create coupon that applies to BOTH products - const coupon = await stripeCli.coupons.create({ - percent_off: 25, - duration: "forever", - applies_to: { - products: [stripeProductId, otherProduct.id], - }, - }); + // Create coupon that applies to BOTH products + const coupon = await stripeCli.coupons.create({ + percent_off: 25, + duration: "forever", + applies_to: { + products: [stripeProductId, otherProduct.id], + }, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 25% off: $100 * 0.75 = $75 - // Total: -$50 + $75 = $25 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.75); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 25% off: $100 * 0.75 = $75 + // Total: -$50 + $75 = $25 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.75); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("applies-to: mixed discounts (one restricted, one unrestricted)")}`, - async () => { - const customerId = "applies-mixed"; +test.concurrent(`${chalk.yellowBright("applies-to: mixed discounts (one restricted, one unrestricted)")}`, async () => { + const customerId = "applies-mixed"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // Create a product that won't match - const otherProduct = await stripeCli.products.create({ - name: `Other Product ${customerId}`, - }); + // Create a product that won't match + const otherProduct = await stripeCli.products.create({ + name: `Other Product ${customerId}`, + }); - // Coupon 1: Unrestricted 10% (applies to all) - const coupon1 = await stripeCli.coupons.create({ - percent_off: 10, - duration: "forever", - }); + // Coupon 1: Unrestricted 10% (applies to all) + const coupon1 = await stripeCli.coupons.create({ + percent_off: 10, + duration: "forever", + }); - // Coupon 2: Restricted 20% (applies to other product, not ours) - const coupon2 = await stripeCli.coupons.create({ - percent_off: 20, - duration: "forever", - applies_to: { - products: [otherProduct.id], - }, - }); + // Coupon 2: Restricted 20% (applies to other product, not ours) + const coupon2 = await stripeCli.coupons.create({ + percent_off: 20, + duration: "forever", + applies_to: { + products: [otherProduct.id], + }, + }); - // Apply both - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon1.id, coupon2.id], - }); + // Apply both + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon1.id, coupon2.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Only the unrestricted 10% coupon applies (20% doesn't match our product) - // Charge with 10% off: $100 * 0.9 = $90 - // Total: -$50 + $90 = $40 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.9); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Only the unrestricted 10% coupon applies (20% doesn't match our product) + // Charge with 10% off: $100 * 0.9 = $90 + // Total: -$50 + $90 = $40 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.9); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); diff --git a/server/tests/integration/billing/update-subscription/discounts/percent-off-discount.test.ts b/server/tests/integration/billing/update-subscription/discounts/percent-off-discount.test.ts index 37d2116f1..f79c34de4 100644 --- a/server/tests/integration/billing/update-subscription/discounts/percent-off-discount.test.ts +++ b/server/tests/integration/billing/update-subscription/discounts/percent-off-discount.test.ts @@ -12,9 +12,9 @@ import { products } from "@tests/utils/fixtures/products.js"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; import chalk from "chalk"; import { - getStripeSubscription, - createPercentCoupon, applySubscriptionDiscount, + createPercentCoupon, + getStripeSubscription, } from "../../utils/discounts/discountTestUtils.js"; const billingUnits = 12; @@ -24,477 +24,454 @@ const pricePerUnit = 10; // MIGRATED TESTS FROM subscription-discounts.test.ts // ============================================================================= -test.concurrent( - `${chalk.yellowBright("percent-off: 20% discount applied to upgrade")}`, - async () => { - const customerId = "pct-20-upgrade"; +test.concurrent(`${chalk.yellowBright("percent-off: 20% discount applied to upgrade")}`, async () => { + const customerId = "pct-20-upgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 20, - }); + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 20, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 20% off: $100 * 0.8 = $80 - // Total: -$50 + $80 = $30 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.8); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 20% off: $100 * 0.8 = $80 + // Total: -$50 + $80 = $30 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.8); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("percent-off: 50% discount applied to upgrade")}`, - async () => { - const customerId = "pct-50-upgrade"; +test.concurrent(`${chalk.yellowBright("percent-off: 50% discount applied to upgrade")}`, async () => { + const customerId = "pct-50-upgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 50, - }); + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 50, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 50% off: $100 * 0.5 = $50 - // Total: -$50 + $50 = $0 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.5); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 50% off: $100 * 0.5 = $50 + // Total: -$50 + $50 = $0 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.5); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("percent-off: 100% discount (free upgrade)")}`, - async () => { - const customerId = "pct-100-free"; +test.concurrent(`${chalk.yellowBright("percent-off: 100% discount (free upgrade)")}`, async () => { + const customerId = "pct-100-free"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 100, - }); + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 100, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 100% off: $100 * 0 = $0 - // Total: -$50 + $0 = -$50 - expect(preview.total).toBe(-50); - }, -); + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 100% off: $100 * 0 = $0 + // Total: -$50 + $0 = -$50 + expect(preview.total).toBe(-50); +}); -test.concurrent( - `${chalk.yellowBright("percent-off: promotion code applied to subscription")}`, - async () => { - const customerId = "pct-promo-code"; +test.concurrent(`${chalk.yellowBright("percent-off: promotion code applied to subscription")}`, async () => { + const customerId = "pct-promo-code"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - // Create a coupon and promotion code - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 25, - }); + // Create a coupon and promotion code + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 25, + }); - const promotionCode = await stripeCli.promotionCodes.create({ - promotion: { - type: "coupon", - coupon: coupon.id, - }, - code: `SAVE25-${customerId}-${Date.now()}`, - }); + const promotionCode = await stripeCli.promotionCodes.create({ + promotion: { + type: "coupon", + coupon: coupon.id, + }, + code: `SAVE25-${customerId}-${Date.now()}`, + }); - // Apply via promotion code - await stripeCli.subscriptions.update(subscription.id, { - discounts: [{ promotion_code: promotionCode.id }], - }); + // Apply via promotion code + await stripeCli.subscriptions.update(subscription.id, { + discounts: [{ promotion_code: promotionCode.id }], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 25% off: $100 * 0.75 = $75 - // Total: -$50 + $75 = $25 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.75); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 25% off: $100 * 0.75 = $75 + // Total: -$50 + $75 = $25 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.75); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); // ============================================================================= // NEW EDGE CASE TESTS // ============================================================================= -test.concurrent( - `${chalk.yellowBright("percent-off: 1% discount (minimum)")}`, - async () => { - const customerId = "pct-1-minimum"; +test.concurrent(`${chalk.yellowBright("percent-off: 1% discount (minimum)")}`, async () => { + const customerId = "pct-1-minimum"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 1, - }); + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 1, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 1% off: $100 * 0.99 = $99 - // Total: -$50 + $99 = $49 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.99); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 1% off: $100 * 0.99 = $99 + // Total: -$50 + $99 = $49 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.99); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("percent-off: 99% discount (near-free)")}`, - async () => { - const customerId = "pct-99-near-free"; +test.concurrent(`${chalk.yellowBright("percent-off: 99% discount (near-free)")}`, async () => { + const customerId = "pct-99-near-free"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 99, - }); + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 99, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }); + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }); - // Upgrade generates: refund (-$50) + charge ($100) - // Discounts only apply to charges, not refunds - // Charge with 99% off: $100 * 0.01 = $1 - // Total: -$50 + $1 = -$49 - const refundAmount = -50; - const discountedCharge = Math.round(100 * 0.01); - const expectedAmount = refundAmount + discountedCharge; + // Upgrade generates: refund (-$50) + charge ($100) + // Discounts only apply to charges, not refunds + // Charge with 99% off: $100 * 0.01 = $1 + // Total: -$50 + $1 = -$49 + const refundAmount = -50; + const discountedCharge = Math.round(100 * 0.01); + const expectedAmount = refundAmount + discountedCharge; - expect(preview.total).toBe(expectedAmount); - }, -); + expect(preview.total).toBe(expectedAmount); +}); -test.concurrent( - `${chalk.yellowBright("percent-off: discount on quantity decrease (refund not discounted)")}`, - async () => { - const customerId = "pct-decrease-refund"; +test.concurrent(`${chalk.yellowBright("percent-off: discount on quantity decrease (refund not discounted)")}`, async () => { + const customerId = "pct-decrease-refund"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - // Start with 10 units - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + // Start with 10 units + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const { stripeCli, subscription } = await getStripeSubscription({ - customerId, - }); + const { stripeCli, subscription } = await getStripeSubscription({ + customerId, + }); - const coupon = await createPercentCoupon({ - stripeCli, - percentOff: 50, - }); + const coupon = await createPercentCoupon({ + stripeCli, + percentOff: 50, + }); - await applySubscriptionDiscount({ - stripeCli, - subscriptionId: subscription.id, - couponIds: [coupon.id], - }); + await applySubscriptionDiscount({ + stripeCli, + subscriptionId: subscription.id, + couponIds: [coupon.id], + }); - // Preview decrease from 10 to 5 units (removing 5 units) - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }); + // Preview decrease from 10 to 5 units (removing 5 units) + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits }], + }); - // Downgrade generates: refund (-$100 for 10 units) + charge ($50 for 5 units) - // Discounts only apply to charges, not refunds - // Charge with 50% off: $50 * 0.5 = $25 - // Total: -$100 + $25 = -$75 - expect(preview.total).toBe(-75); - }, -); + // Downgrade generates: refund (-$100 for 10 units) + charge ($50 for 5 units) + // Discounts only apply to charges, not refunds + // Charge with 50% off: $50 * 0.5 = $25 + // Total: -$100 + $25 = -$75 + expect(preview.total).toBe(-75); +}); diff --git a/server/tests/integration/billing/update-subscription/invoice/update-quantity-invoice-mode.test.ts b/server/tests/integration/billing/update-subscription/invoice/update-quantity-invoice-mode.test.ts index c98de3997..8d824da09 100644 --- a/server/tests/integration/billing/update-subscription/invoice/update-quantity-invoice-mode.test.ts +++ b/server/tests/integration/billing/update-subscription/invoice/update-quantity-invoice-mode.test.ts @@ -22,390 +22,370 @@ const billingUnits = 12; * - Entitlements after payment (via checkout) */ -test.concurrent( - `${chalk.yellowBright("update-quantity: default invoice mode (draft, immediate entitlements)")}`, - async () => { - const customerId = "inv-mode-default"; +test.concurrent(`${chalk.yellowBright("update-quantity: default invoice mode (draft, immediate entitlements)")}`, async () => { + const customerId = "inv-mode-default"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const beforeUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + const beforeUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const customerProduct = beforeUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const beforeEntitlement = customerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const beforeBalance = beforeEntitlement?.balance || 0; + const customerProduct = beforeUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const beforeEntitlement = customerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const beforeBalance = beforeEntitlement?.balance || 0; - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 15 * billingUnits }, - ], - invoice: true, - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 15 * billingUnits }, + ], + invoice: true, + }); - const afterUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + const afterUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const afterCustomerProduct = afterUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const afterBalance = afterEntitlement?.balance || 0; + const afterCustomerProduct = afterUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const afterBalance = afterEntitlement?.balance || 0; - expect(afterBalance).toBe(beforeBalance + 60); + expect(afterBalance).toBe(beforeBalance + 60); - const customer = await autumnV1.customers.get(customerId); - const feature = customer.features?.[TestFeature.Messages]; - expect(feature?.balance).toBe(180); + const customer = await autumnV1.customers.get(customerId); + const feature = customer.features?.[TestFeature.Messages]; + expect(feature?.balance).toBe(180); - const draftInvoice = customer.invoices?.find( - (inv) => inv.status === "draft", - ); - expect(draftInvoice).toBeDefined(); - }, -); + const draftInvoice = customer.invoices?.find((inv) => inv.status === "draft"); + expect(draftInvoice).toBeDefined(); +}); -test.concurrent( - `${chalk.yellowBright("update-quantity: draft invoice with immediate entitlements (explicit)")}`, - async () => { - const customerId = "inv-mode-draft-explicit"; +test.concurrent(`${chalk.yellowBright("update-quantity: draft invoice with immediate entitlements (explicit)")}`, async () => { + const customerId = "inv-mode-draft-explicit"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const beforeUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + const beforeUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const customerProduct = beforeUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const beforeEntitlement = customerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const beforeBalance = beforeEntitlement?.balance || 0; + const customerProduct = beforeUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const beforeEntitlement = customerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const beforeBalance = beforeEntitlement?.balance || 0; - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 15 * billingUnits }, - ], - invoice: true, - finalize_invoice: false, - enable_product_immediately: true, - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 15 * billingUnits }, + ], + invoice: true, + finalize_invoice: false, + enable_product_immediately: true, + }); - // Entitlements should be updated immediately - const afterUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + // Entitlements should be updated immediately + const afterUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const afterCustomerProduct = afterUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const afterBalance = afterEntitlement?.balance || 0; + const afterCustomerProduct = afterUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const afterBalance = afterEntitlement?.balance || 0; - // +5 units × 12 billing_units = +60 messages - expect(afterBalance).toBe(beforeBalance + 60); + // +5 units × 12 billing_units = +60 messages + expect(afterBalance).toBe(beforeBalance + 60); - // Verify via API that balance is updated and invoice is draft - const customer = await autumnV1.customers.get(customerId); - const feature = customer.features?.[TestFeature.Messages]; - expect(feature?.balance).toBe(180); // 15 units × 12 = 180 + // Verify via API that balance is updated and invoice is draft + const customer = await autumnV1.customers.get(customerId); + const feature = customer.features?.[TestFeature.Messages]; + expect(feature?.balance).toBe(180); // 15 units × 12 = 180 - const draftInvoice = customer.invoices?.find( - (inv) => inv.status === "draft", - ); - expect(draftInvoice).toBeDefined(); - }, -); + const draftInvoice = customer.invoices?.find((inv) => inv.status === "draft"); + expect(draftInvoice).toBeDefined(); +}); -test.concurrent( - `${chalk.yellowBright("update-quantity: finalized invoice with immediate entitlements")}`, - async () => { - const customerId = "inv-mode-finalized"; +test.concurrent(`${chalk.yellowBright("update-quantity: finalized invoice with immediate entitlements")}`, async () => { + const customerId = "inv-mode-finalized"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const beforeUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + const beforeUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const customerProduct = beforeUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const beforeEntitlement = customerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const beforeBalance = beforeEntitlement?.balance || 0; + const customerProduct = beforeUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const beforeEntitlement = customerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const beforeBalance = beforeEntitlement?.balance || 0; - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, - ], - invoice: true, - finalize_invoice: true, - enable_product_immediately: true, - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, + ], + invoice: true, + finalize_invoice: true, + enable_product_immediately: true, + }); - // Entitlements should be updated immediately - const afterUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + // Entitlements should be updated immediately + const afterUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const afterCustomerProduct = afterUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const afterBalance = afterEntitlement?.balance || 0; + const afterCustomerProduct = afterUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const afterBalance = afterEntitlement?.balance || 0; - // +10 units × 12 billing_units = +120 messages - expect(afterBalance).toBe(beforeBalance + 120); + // +10 units × 12 billing_units = +120 messages + expect(afterBalance).toBe(beforeBalance + 120); - // Verify via API that balance is updated and invoice is paid - const customer = await autumnV1.customers.get(customerId); - const feature = customer.features?.[TestFeature.Messages]; - expect(feature?.balance).toBe(240); // 20 units × 12 = 240 + // Verify via API that balance is updated and invoice is paid + const customer = await autumnV1.customers.get(customerId); + const feature = customer.features?.[TestFeature.Messages]; + expect(feature?.balance).toBe(240); // 20 units × 12 = 240 - const paidInvoice = customer.invoices?.find( - (inv) => inv.status === "paid", - ); - expect(paidInvoice).toBeDefined(); - }, -); + const paidInvoice = customer.invoices?.find((inv) => inv.status === "paid"); + expect(paidInvoice).toBeDefined(); +}); -test.concurrent( - `${chalk.yellowBright("update-quantity: entitlements after payment via checkout")}`, - async () => { - const customerId = "inv-mode-payment-required"; +test.concurrent(`${chalk.yellowBright("update-quantity: entitlements after payment via checkout")}`, async () => { + const customerId = "inv-mode-payment-required"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const beforeUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + const beforeUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const customerProduct = beforeUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const beforeEntitlement = customerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const beforeBalance = beforeEntitlement?.balance || 0; + const customerProduct = beforeUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const beforeEntitlement = customerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const beforeBalance = beforeEntitlement?.balance || 0; - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 25 * billingUnits }, - ], - invoice: true, - finalize_invoice: true, - enable_product_immediately: false, - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 25 * billingUnits }, + ], + invoice: true, + finalize_invoice: true, + enable_product_immediately: false, + }); - // Entitlements should NOT be updated yet (waiting for payment) - const afterUpdate = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + // Entitlements should NOT be updated yet (waiting for payment) + const afterUpdate = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const afterCustomerProduct = afterUpdate.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const afterBalance = afterEntitlement?.balance || 0; + const afterCustomerProduct = afterUpdate.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const afterEntitlement = afterCustomerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const afterBalance = afterEntitlement?.balance || 0; - // Balance should remain unchanged until payment - expect(afterBalance).toBe(beforeBalance); + // Balance should remain unchanged until payment + expect(afterBalance).toBe(beforeBalance); - // Verify via API that balance is NOT updated and invoice is open - const customer = await autumnV1.customers.get(customerId); - const feature = customer.features?.[TestFeature.Messages]; - expect(feature?.balance).toBe(120); // Still 10 units × 12 = 120 + // Verify via API that balance is NOT updated and invoice is open + const customer = await autumnV1.customers.get(customerId); + const feature = customer.features?.[TestFeature.Messages]; + expect(feature?.balance).toBe(120); // Still 10 units × 12 = 120 - const openInvoice = customer.invoices?.find( - (inv) => inv.status === "open", - ); - expect(openInvoice).toBeDefined(); - expect(openInvoice?.hosted_invoice_url).toBeDefined(); + const openInvoice = customer.invoices?.find((inv) => inv.status === "open"); + expect(openInvoice).toBeDefined(); + expect(openInvoice?.hosted_invoice_url).toBeDefined(); - // Complete payment via checkout using Puppeteer - await completeInvoiceCheckout({ - url: openInvoice!.hosted_invoice_url!, - }); + // Complete payment via checkout using Puppeteer + await completeInvoiceCheckout({ + url: openInvoice!.hosted_invoice_url!, + }); - // Wait for webhook processing - await timeout(10000); + // Wait for webhook processing + await timeout(10000); - // Entitlements should now be updated after payment - const afterPayment = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + // Entitlements should now be updated after payment + const afterPayment = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const paidCustomerProduct = afterPayment.customer_products.find( - (cp) => cp.product.id === product.id, - ); - const paidEntitlement = paidCustomerProduct?.customer_entitlements.find( - (ent) => ent.entitlement.feature_id === TestFeature.Messages, - ); - const paidBalance = paidEntitlement?.balance || 0; + const paidCustomerProduct = afterPayment.customer_products.find( + (cp) => cp.product.id === product.id, + ); + const paidEntitlement = paidCustomerProduct?.customer_entitlements.find( + (ent) => ent.entitlement.feature_id === TestFeature.Messages, + ); + const paidBalance = paidEntitlement?.balance || 0; - // +15 units × 12 billing_units = +180 messages - expect(paidBalance).toBe(beforeBalance + 180); + // +15 units × 12 billing_units = +180 messages + expect(paidBalance).toBe(beforeBalance + 180); - // Verify via API that balance is now updated and invoice is paid - const customerAfterPayment = - await autumnV1.customers.get(customerId); - const featureAfterPayment = - customerAfterPayment.features?.[TestFeature.Messages]; - expect(featureAfterPayment?.balance).toBe(300); // 25 units × 12 = 300 + // Verify via API that balance is now updated and invoice is paid + const customerAfterPayment = + await autumnV1.customers.get(customerId); + const featureAfterPayment = + customerAfterPayment.features?.[TestFeature.Messages]; + expect(featureAfterPayment?.balance).toBe(300); // 25 units × 12 = 300 - // All invoices should now be paid - const unpaidInvoices = customerAfterPayment.invoices?.filter( - (inv) => inv.status !== "paid", - ); - expect(unpaidInvoices?.length ?? 0).toBe(0); - }, -); + // All invoices should now be paid + const unpaidInvoices = customerAfterPayment.invoices?.filter( + (inv) => inv.status !== "paid", + ); + expect(unpaidInvoices?.length ?? 0).toBe(0); +}); diff --git a/server/tests/integration/billing/update-subscription/update-quantity/increase-quantity.test.ts b/server/tests/integration/billing/update-subscription/update-quantity/increase-quantity.test.ts index 0b051c11a..24842589d 100644 --- a/server/tests/integration/billing/update-subscription/update-quantity/increase-quantity.test.ts +++ b/server/tests/integration/billing/update-subscription/update-quantity/increase-quantity.test.ts @@ -17,275 +17,266 @@ import chalk from "chalk"; * - Selective upgrade (one feature changed, others unchanged) */ -test.concurrent( - `${chalk.yellowBright("update-quantity: upgrade 10 to 20 units")}`, - async () => { - const customerId = "inc-qty-basic-upgrade"; - const billingUnits = 12; - const pricePerUnit = 8; +test.concurrent(`${chalk.yellowBright("update-quantity: upgrade 10 to 20 units")}`, async () => { + const customerId = "inc-qty-basic-upgrade"; + const billingUnits = 12; + const pricePerUnit = 8; - const prepaidItem = items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - price: pricePerUnit, - }); + const prepaidItem = items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + price: pricePerUnit, + }); - const product = products.base({ - id: "prepaid", - items: [prepaidItem], - }); + const product = products.base({ + id: "prepaid", + items: [prepaidItem], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - // Preview the upgrade - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, - ], - }); + // Preview the upgrade + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, + ], + }); - // Verify preview total matches expected - expect(preview.total).toBe(10 * pricePerUnit); + // Verify preview total matches expected + expect(preview.total).toBe(10 * pricePerUnit); - // Upgrade from 10 to 20 units - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, - ], - }); + // Upgrade from 10 to 20 units + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, + ], + }); - const customer = await autumnV1.customers.get(customerId); - const feature = customer.features?.[TestFeature.Messages]; + const customer = await autumnV1.customers.get(customerId); + const feature = customer.features?.[TestFeature.Messages]; - // Should have 240 messages (20 units × 12 billing_units) - expect(feature?.balance).toBe(240); + // Should have 240 messages (20 units × 12 billing_units) + expect(feature?.balance).toBe(240); - // Expect upgrade invoice for +10 units (10 * $8 = $80 = 8000 cents) - expectLatestInvoiceCorrect({ - customer, - productId: product.id, - amount: 10 * pricePerUnit, - }); - }, -); + // Expect upgrade invoice for +10 units (10 * $8 = $80 = 8000 cents) + expectLatestInvoiceCorrect({ + customer, + productId: product.id, + amount: 10 * pricePerUnit, + }); +}); -test.concurrent( - `${chalk.yellowBright("update-quantity: upgrade multiple features simultaneously")}`, - async () => { - const customerId = "inc-qty-multi-feature"; - const messagesBillingUnits = 10; - const wordsBillingUnits = 100; - const messagesPrice = 5; - const wordsPrice = 10; // default from items.prepaid +test.concurrent(`${chalk.yellowBright("update-quantity: upgrade multiple features simultaneously")}`, async () => { + const customerId = "inc-qty-multi-feature"; + const messagesBillingUnits = 10; + const wordsBillingUnits = 100; + const messagesPrice = 5; + const wordsPrice = 10; // default from items.prepaid - const messagesItem = items.prepaid({ - featureId: TestFeature.Messages, - billingUnits: messagesBillingUnits, - price: messagesPrice, - }); + const messagesItem = items.prepaid({ + featureId: TestFeature.Messages, + billingUnits: messagesBillingUnits, + price: messagesPrice, + }); - const wordsItem = items.prepaid({ - featureId: TestFeature.Words, - billingUnits: wordsBillingUnits, - price: wordsPrice, - }); + const wordsItem = items.prepaid({ + featureId: TestFeature.Words, + billingUnits: wordsBillingUnits, + price: wordsPrice, + }); - const product = products.base({ - id: "multi_feature", - items: [messagesItem, wordsItem], - }); + const product = products.base({ + id: "multi_feature", + items: [messagesItem, wordsItem], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 5 * messagesBillingUnits, - }, - { - feature_id: TestFeature.Words, - quantity: 2 * wordsBillingUnits, - }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 5 * messagesBillingUnits, + }, + { + feature_id: TestFeature.Words, + quantity: 2 * wordsBillingUnits, + }, + ], + }), + ], + }); - // Invoice total: Messages (5->10 units = +5 * $5) + Words (2->5 units = +3 * $10) - const expectedAmount = 5 * messagesPrice + 3 * wordsPrice; + // Invoice total: Messages (5->10 units = +5 * $5) + Words (2->5 units = +3 * $10) + const expectedAmount = 5 * messagesPrice + 3 * wordsPrice; - // Preview the upgrade - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 10 * messagesBillingUnits, - }, - { - feature_id: TestFeature.Words, - quantity: 5 * wordsBillingUnits, - }, - ], - }); + // Preview the upgrade + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 10 * messagesBillingUnits, + }, + { + feature_id: TestFeature.Words, + quantity: 5 * wordsBillingUnits, + }, + ], + }); - // Verify preview total matches expected - expect(preview.total).toBe(expectedAmount); + // Verify preview total matches expected + expect(preview.total).toBe(expectedAmount); - // Upgrade both features - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 10 * messagesBillingUnits, - }, - { - feature_id: TestFeature.Words, - quantity: 5 * wordsBillingUnits, - }, - ], - }); + // Upgrade both features + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 10 * messagesBillingUnits, + }, + { + feature_id: TestFeature.Words, + quantity: 5 * wordsBillingUnits, + }, + ], + }); - const customer = await autumnV1.customers.get(customerId); + const customer = await autumnV1.customers.get(customerId); - expect(customer.features?.[TestFeature.Messages]?.balance).toBe(100); - expect(customer.features?.[TestFeature.Words]?.balance).toBe(500); + expect(customer.features?.[TestFeature.Messages]?.balance).toBe(100); + expect(customer.features?.[TestFeature.Words]?.balance).toBe(500); - expectLatestInvoiceCorrect({ - customer, - productId: product.id, - amount: expectedAmount, - }); - }, -); + expectLatestInvoiceCorrect({ + customer, + productId: product.id, + amount: expectedAmount, + }); +}); -test.concurrent( - `${chalk.yellowBright("update-quantity: upgrade one feature, keep other unchanged")}`, - async () => { - const customerId = "inc-qty-selective"; - const messagesBillingUnits = 10; - const wordsBillingUnits = 100; - const messagesPrice = 5; +test.concurrent(`${chalk.yellowBright("update-quantity: upgrade one feature, keep other unchanged")}`, async () => { + const customerId = "inc-qty-selective"; + const messagesBillingUnits = 10; + const wordsBillingUnits = 100; + const messagesPrice = 5; - const product = products.base({ - id: "selective_upgrade", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits: messagesBillingUnits, - price: messagesPrice, - }), - items.prepaid({ - featureId: TestFeature.Words, - billingUnits: wordsBillingUnits, - }), - ], - }); + const product = products.base({ + id: "selective_upgrade", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits: messagesBillingUnits, + price: messagesPrice, + }), + items.prepaid({ + featureId: TestFeature.Words, + billingUnits: wordsBillingUnits, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 10 * messagesBillingUnits, - }, - { - feature_id: TestFeature.Words, - quantity: 5 * wordsBillingUnits, - }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 10 * messagesBillingUnits, + }, + { + feature_id: TestFeature.Words, + quantity: 5 * wordsBillingUnits, + }, + ], + }), + ], + }); - // Preview the upgrade - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: product.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 15 * messagesBillingUnits, - }, - { - feature_id: TestFeature.Words, - quantity: 5 * wordsBillingUnits, // unchanged - }, - ], - }); + // Preview the upgrade + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: product.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 15 * messagesBillingUnits, + }, + { + feature_id: TestFeature.Words, + quantity: 5 * wordsBillingUnits, // unchanged + }, + ], + }); - // Verify preview total matches expected (10->15 = +5 units * $5) - expect(preview.total).toBe(5 * messagesPrice); + // Verify preview total matches expected (10->15 = +5 units * $5) + expect(preview.total).toBe(5 * messagesPrice); - // Upgrade only messages, keep words unchanged - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { - feature_id: TestFeature.Messages, - quantity: 15 * messagesBillingUnits, - }, - { - feature_id: TestFeature.Words, - quantity: 5 * wordsBillingUnits, // unchanged - }, - ], - }); + // Upgrade only messages, keep words unchanged + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 15 * messagesBillingUnits, + }, + { + feature_id: TestFeature.Words, + quantity: 5 * wordsBillingUnits, // unchanged + }, + ], + }); - const customer = await autumnV1.customers.get(customerId); + const customer = await autumnV1.customers.get(customerId); - expect(customer.features?.[TestFeature.Messages]?.balance).toBe(150); - expect(customer.features?.[TestFeature.Words]?.balance).toBe(500); + expect(customer.features?.[TestFeature.Messages]?.balance).toBe(150); + expect(customer.features?.[TestFeature.Words]?.balance).toBe(500); - // Invoice should only have messages upgrade (10->15 = +5 units * $5) - expectLatestInvoiceCorrect({ - customer, - productId: product.id, - amount: 5 * messagesPrice, - }); + // Invoice should only have messages upgrade (10->15 = +5 units * $5) + expectLatestInvoiceCorrect({ + customer, + productId: product.id, + amount: 5 * messagesPrice, + }); - // Verify Stripe invoice has only 2 line items (credit + debit for messages only) - const stripeInvoices = await ctx.stripeCli.invoices.list({ - customer: customer.stripe_id!, - }); + // Verify Stripe invoice has only 2 line items (credit + debit for messages only) + const stripeInvoices = await ctx.stripeCli.invoices.list({ + customer: customer.stripe_id!, + }); - const latestInvoice = stripeInvoices.data[0]; - expect(latestInvoice.lines.data.length).toBe(2); - }, -); + const latestInvoice = stripeInvoices.data[0]; + expect(latestInvoice.lines.data.length).toBe(2); +}); diff --git a/server/tests/integration/billing/update-subscription/update-quantity/invoicing.test.ts b/server/tests/integration/billing/update-subscription/update-quantity/invoicing.test.ts index 013987226..d1792d1ca 100644 --- a/server/tests/integration/billing/update-subscription/update-quantity/invoicing.test.ts +++ b/server/tests/integration/billing/update-subscription/update-quantity/invoicing.test.ts @@ -16,58 +16,55 @@ const billingUnits = 12; * invoice creation and finalization behavior. */ -test.concurrent( - `${chalk.yellowBright("update-quantity: create invoice on upgrade")}`, - async () => { - const customerId = "invoicing-upgrade"; +test.concurrent(`${chalk.yellowBright("update-quantity: create invoice on upgrade")}`, async () => { + const customerId = "invoicing-upgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + }), + ], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 10 * billingUnits }, + ], + }), + ], + }); - const beforeUpdate = await autumnV1.customers.get(customerId); - const invoiceCountBefore = beforeUpdate.invoices?.length || 0; + const beforeUpdate = await autumnV1.customers.get(customerId); + const invoiceCountBefore = beforeUpdate.invoices?.length || 0; - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, - ], - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 20 * billingUnits }, + ], + }); - const afterUpdate = await autumnV1.customers.get(customerId); - const invoiceCountAfter = afterUpdate.invoices?.length || 0; + const afterUpdate = await autumnV1.customers.get(customerId); + const invoiceCountAfter = afterUpdate.invoices?.length || 0; - // Should have created a new prorated invoice - expect(invoiceCountAfter).toBeGreaterThan(invoiceCountBefore); + // Should have created a new prorated invoice + expect(invoiceCountAfter).toBeGreaterThan(invoiceCountBefore); - const latestInvoice = afterUpdate.invoices?.[0]; - expect(latestInvoice).toBeDefined(); - expect(latestInvoice?.status).toBe("paid"); - // Invoice total should be prorated amount for 10 additional units - expect(latestInvoice?.total).toBeGreaterThan(0); - }, -); + const latestInvoice = afterUpdate.invoices?.[0]; + expect(latestInvoice).toBeDefined(); + expect(latestInvoice?.status).toBe("paid"); + // Invoice total should be prorated amount for 10 additional units + expect(latestInvoice?.total).toBeGreaterThan(0); +}); diff --git a/server/tests/integration/billing/update-subscription/update-quantity/no-change-quantity.test.ts b/server/tests/integration/billing/update-subscription/update-quantity/no-change-quantity.test.ts index ecd26f98f..e1b4c458b 100644 --- a/server/tests/integration/billing/update-subscription/update-quantity/no-change-quantity.test.ts +++ b/server/tests/integration/billing/update-subscription/update-quantity/no-change-quantity.test.ts @@ -15,60 +15,53 @@ import chalk from "chalk"; const billingUnits = 12; -test.concurrent( - `${chalk.yellowBright("update-quantity: same quantity is no-op")}`, - async () => { - const customerId = "no-change-qty-noop"; +test.concurrent(`${chalk.yellowBright("update-quantity: same quantity is no-op")}`, async () => { + const customerId = "no-change-qty-noop"; - const prepaidItem = items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - }); + const prepaidItem = items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + }); - const product = products.base({ - id: "prepaid", - items: [prepaidItem], - }); + const product = products.base({ + id: "prepaid", + items: [prepaidItem], + }); - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, + ], + }), + ], + }); - const beforeUpdate = - await autumnV1.customers.get(customerId); - const beforeFeature = beforeUpdate.features?.[TestFeature.Messages]; - const beforeInvoiceCount = beforeUpdate.invoices?.length ?? 0; + const beforeUpdate = await autumnV1.customers.get(customerId); + const beforeFeature = beforeUpdate.features?.[TestFeature.Messages]; + const beforeInvoiceCount = beforeUpdate.invoices?.length ?? 0; - // Update to same quantity (no-op) - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: 5 * billingUnits }, - ], - }); + // Update to same quantity (no-op) + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits }], + }); - const afterUpdate = - await autumnV1.customers.get(customerId); - const afterFeature = afterUpdate.features?.[TestFeature.Messages]; - const afterInvoiceCount = afterUpdate.invoices?.length ?? 0; + const afterUpdate = await autumnV1.customers.get(customerId); + const afterFeature = afterUpdate.features?.[TestFeature.Messages]; + const afterInvoiceCount = afterUpdate.invoices?.length ?? 0; - // Balance should remain the same - expect(afterFeature?.balance).toBe(beforeFeature?.balance); + // Balance should remain the same + expect(afterFeature?.balance).toBe(beforeFeature?.balance); - // No-op should not create a new invoice - expect(afterInvoiceCount).toBe(beforeInvoiceCount); - }, -); + // No-op should not create a new invoice + expect(afterInvoiceCount).toBe(beforeInvoiceCount); +}); diff --git a/server/tests/integration/billing/update-subscription/update-quantity/quantity-stripe-sync.test.ts b/server/tests/integration/billing/update-subscription/update-quantity/quantity-stripe-sync.test.ts index aa41e74b9..4309506bb 100644 --- a/server/tests/integration/billing/update-subscription/update-quantity/quantity-stripe-sync.test.ts +++ b/server/tests/integration/billing/update-subscription/update-quantity/quantity-stripe-sync.test.ts @@ -25,204 +25,192 @@ import { CusService } from "@/internal/customers/CusService.js"; const billingUnits = 12; -test.concurrent( - `${chalk.yellowBright("update-quantity: stripe sync upgrade quantity difference")}`, - async () => { - const customerId = "qty-stripe-sync-upgrade"; +test.concurrent(`${chalk.yellowBright("update-quantity: stripe sync upgrade quantity difference")}`, async () => { + const customerId = "qty-stripe-sync-upgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - config: { - on_increase: OnIncrease.ProrateImmediately, - on_decrease: OnDecrease.NoProrations, - }, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.NoProrations, + }, + }), + ], + }); - // Initial: 5 units (60 messages) - const initialUnits = 5; - const initialQuantity = initialUnits * billingUnits; + // Initial: 5 units (60 messages) + const initialUnits = 5; + const initialQuantity = initialUnits * billingUnits; - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: initialQuantity }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: initialQuantity }, + ], + }), + ], + }); - const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env }); + const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env }); - // Get Stripe subscription item quantity before update - const fullCustomer = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + // Get Stripe subscription item quantity before update + const fullCustomer = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const stripeCustomerId = - fullCustomer.processor?.id || fullCustomer.processor?.processor_id; - expect(stripeCustomerId).toBeDefined(); + const stripeCustomerId = + fullCustomer.processor?.id || fullCustomer.processor?.processor_id; + expect(stripeCustomerId).toBeDefined(); - const subscriptionsBefore = await stripeCli.subscriptions.list({ - customer: stripeCustomerId as string, - status: "all", - }); + const subscriptionsBefore = await stripeCli.subscriptions.list({ + customer: stripeCustomerId as string, + status: "all", + }); - expect(subscriptionsBefore.data.length).toBeGreaterThan(0); - const subscription = subscriptionsBefore.data[0]; + expect(subscriptionsBefore.data.length).toBeGreaterThan(0); + const subscription = subscriptionsBefore.data[0]; - const subscriptionItemBefore = subscription.items.data.find( - (item) => item.quantity !== undefined && item.quantity > 0, - ); - expect(subscriptionItemBefore).toBeDefined(); - expect(subscriptionItemBefore!.quantity).toBe(initialUnits); + const subscriptionItemBefore = subscription.items.data.find( + (item) => item.quantity !== undefined && item.quantity > 0, + ); + expect(subscriptionItemBefore).toBeDefined(); + expect(subscriptionItemBefore!.quantity).toBe(initialUnits); - // Update: 5 -> 8 units (difference = +3 units) - const updatedUnits = 8; - const updatedQuantity = updatedUnits * billingUnits; - const unitsDifference = updatedUnits - initialUnits; // +3 + // Update: 5 -> 8 units (difference = +3 units) + const updatedUnits = 8; + const updatedQuantity = updatedUnits * billingUnits; + const unitsDifference = updatedUnits - initialUnits; // +3 - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: updatedQuantity }, - ], - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [{ feature_id: TestFeature.Messages, quantity: updatedQuantity }], + }); - // Verify Stripe subscription item is updated correctly - const subscriptionsAfter = await stripeCli.subscriptions.list({ - customer: stripeCustomerId as string, - status: "all", - }); + // Verify Stripe subscription item is updated correctly + const subscriptionsAfter = await stripeCli.subscriptions.list({ + customer: stripeCustomerId as string, + status: "all", + }); - const subscriptionAfter = subscriptionsAfter.data[0]; - const subscriptionItemAfter = subscriptionAfter.items.data.find( - (item) => item.id === subscriptionItemBefore!.id, - ); + const subscriptionAfter = subscriptionsAfter.data[0]; + const subscriptionItemAfter = subscriptionAfter.items.data.find( + (item) => item.id === subscriptionItemBefore!.id, + ); - expect(subscriptionItemAfter).toBeDefined(); + expect(subscriptionItemAfter).toBeDefined(); - // In the simple case (one product), both approaches give the same result: - // Absolute: quantity = 8 - // Difference: quantity = 5 + 3 = 8 - // This test verifies the mechanism works, but the bug manifests with multiple products - expect(subscriptionItemAfter!.quantity).toBe(updatedUnits); + // In the simple case (one product), both approaches give the same result: + // Absolute: quantity = 8 + // Difference: quantity = 5 + 3 = 8 + // This test verifies the mechanism works, but the bug manifests with multiple products + expect(subscriptionItemAfter!.quantity).toBe(updatedUnits); - // Verify the stripeSubscriptionItemQuantityDifference is being calculated correctly - // by checking that the final quantity matches initialUnits + unitsDifference - expect(subscriptionItemAfter!.quantity).toBe( - initialUnits + unitsDifference, - ); - }, -); + // Verify the stripeSubscriptionItemQuantityDifference is being calculated correctly + // by checking that the final quantity matches initialUnits + unitsDifference + expect(subscriptionItemAfter!.quantity).toBe(initialUnits + unitsDifference); +}); -test.concurrent( - `${chalk.yellowBright("update-quantity: stripe sync downgrade quantity difference")}`, - async () => { - const customerId = "qty-stripe-sync-downgrade"; +test.concurrent(`${chalk.yellowBright("update-quantity: stripe sync downgrade quantity difference")}`, async () => { + const customerId = "qty-stripe-sync-downgrade"; - const product = products.base({ - id: "prepaid", - items: [ - items.prepaid({ - featureId: TestFeature.Messages, - billingUnits, - config: { - on_increase: OnIncrease.ProrateImmediately, - on_decrease: OnDecrease.NoProrations, - }, - }), - ], - }); + const product = products.base({ + id: "prepaid", + items: [ + items.prepaid({ + featureId: TestFeature.Messages, + billingUnits, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.NoProrations, + }, + }), + ], + }); - // Start with 8 units (96 messages) - const initialUnits = 8; - const initialQuantity = initialUnits * billingUnits; + // Start with 8 units (96 messages) + const initialUnits = 8; + const initialQuantity = initialUnits * billingUnits; - const { autumnV1 } = await initScenario({ - customerId, - setup: [ - s.customer({ paymentMethod: "success" }), - s.products({ list: [product] }), - ], - actions: [ - s.attach({ - productId: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: initialQuantity }, - ], - }), - ], - }); + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [product] }), + ], + actions: [ + s.attach({ + productId: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: initialQuantity }, + ], + }), + ], + }); - const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env }); + const stripeCli = createStripeCli({ org: ctx.org, env: ctx.env }); - // Get Stripe subscription before downgrade - const fullCustomer = await CusService.getFull({ - db: ctx.db, - idOrInternalId: customerId, - orgId: ctx.org.id, - env: ctx.env, - }); + // Get Stripe subscription before downgrade + const fullCustomer = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); - const stripeCustomerId = - fullCustomer.processor?.id || fullCustomer.processor?.processor_id; + const stripeCustomerId = + fullCustomer.processor?.id || fullCustomer.processor?.processor_id; - const subscriptionsBefore = await stripeCli.subscriptions.list({ - customer: stripeCustomerId as string, - status: "all", - }); + const subscriptionsBefore = await stripeCli.subscriptions.list({ + customer: stripeCustomerId as string, + status: "all", + }); - const subscription = subscriptionsBefore.data[0]; - const subscriptionItemBefore = subscription.items.data.find( - (item) => item.quantity !== undefined && item.quantity > 0, - ); - expect(subscriptionItemBefore).toBeDefined(); - expect(subscriptionItemBefore!.quantity).toBe(initialUnits); + const subscription = subscriptionsBefore.data[0]; + const subscriptionItemBefore = subscription.items.data.find( + (item) => item.quantity !== undefined && item.quantity > 0, + ); + expect(subscriptionItemBefore).toBeDefined(); + expect(subscriptionItemBefore!.quantity).toBe(initialUnits); - // Downgrade: 8 -> 3 units (difference = -5 units) - const downgradedUnits = 3; - const downgradedQuantity = downgradedUnits * billingUnits; - const unitsDifference = downgradedUnits - initialUnits; // -5 + // Downgrade: 8 -> 3 units (difference = -5 units) + const downgradedUnits = 3; + const downgradedQuantity = downgradedUnits * billingUnits; + const unitsDifference = downgradedUnits - initialUnits; // -5 - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: product.id, - options: [ - { feature_id: TestFeature.Messages, quantity: downgradedQuantity }, - ], - }); + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: product.id, + options: [ + { feature_id: TestFeature.Messages, quantity: downgradedQuantity }, + ], + }); - const subscriptionsAfter = await stripeCli.subscriptions.list({ - customer: stripeCustomerId as string, - status: "all", - }); + const subscriptionsAfter = await stripeCli.subscriptions.list({ + customer: stripeCustomerId as string, + status: "all", + }); - const subscriptionAfter = subscriptionsAfter.data[0]; - const subscriptionItemAfter = subscriptionAfter.items.data.find( - (item) => item.id === subscriptionItemBefore!.id, - ); + const subscriptionAfter = subscriptionsAfter.data[0]; + const subscriptionItemAfter = subscriptionAfter.items.data.find( + (item) => item.id === subscriptionItemBefore!.id, + ); - expect(subscriptionItemAfter).toBeDefined(); - expect(subscriptionItemAfter!.quantity).toBe(downgradedUnits); - expect(subscriptionItemAfter!.quantity).toBe( - initialUnits + unitsDifference, - ); - }, -); + expect(subscriptionItemAfter).toBeDefined(); + expect(subscriptionItemAfter!.quantity).toBe(downgradedUnits); + expect(subscriptionItemAfter!.quantity).toBe(initialUnits + unitsDifference); +}); diff --git a/server/tests/integration/billing/utils/discounts/discountTestUtils.ts b/server/tests/integration/billing/utils/discounts/discountTestUtils.ts index 98fdffdd6..822488092 100644 --- a/server/tests/integration/billing/utils/discounts/discountTestUtils.ts +++ b/server/tests/integration/billing/utils/discounts/discountTestUtils.ts @@ -2,10 +2,10 @@ * Shared utilities for discount integration tests. */ +import ctx from "@tests/utils/testInitUtils/createTestContext.js"; import type Stripe from "stripe"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusService } from "@/internal/customers/CusService.js"; -import ctx from "@tests/utils/testInitUtils/createTestContext.js"; /** * Get Stripe subscription and client for a customer. @@ -69,7 +69,9 @@ export const createPercentCoupon = async ({ ...(duration === "repeating" && durationInMonths ? { duration_in_months: durationInMonths } : {}), - ...(appliesToProducts ? { applies_to: { products: appliesToProducts } } : {}), + ...(appliesToProducts + ? { applies_to: { products: appliesToProducts } } + : {}), }); }; @@ -95,7 +97,9 @@ export const createAmountCoupon = async ({ currency, duration: "repeating", duration_in_months: durationInMonths, - ...(appliesToProducts ? { applies_to: { products: appliesToProducts } } : {}), + ...(appliesToProducts + ? { applies_to: { products: appliesToProducts } } + : {}), }); }; diff --git a/server/tests/integration/billing/utils/expectCustomerProductCorrect.ts b/server/tests/integration/billing/utils/expectCustomerProductCorrect.ts index 393c08056..ed04d18b7 100644 --- a/server/tests/integration/billing/utils/expectCustomerProductCorrect.ts +++ b/server/tests/integration/billing/utils/expectCustomerProductCorrect.ts @@ -91,13 +91,52 @@ export const expectProductCanceling = async (params: { }) => expectCustomerProductCorrect({ ...params, state: "canceled" }); /** - * Shorthand for checking product is scheduled + * Shorthand for checking product is scheduled. + * Optionally verify the `started_at` timestamp is within a tolerance of the expected value. + * + * @param startsAt - Expected timestamp in milliseconds when the product will start + * @param toleranceMs - Allowed deviation in milliseconds (default: 2 minutes) */ -export const expectProductScheduled = async (params: { +export const expectProductScheduled = async ({ + customerId, + customer: providedCustomer, + productId, + startsAt, + toleranceMs = 2 * 60 * 1000, +}: { customerId?: string; customer?: ApiCustomerV3 | ApiEntityV0; productId: string; -}) => expectCustomerProductCorrect({ ...params, state: "scheduled" }); + startsAt?: number; + toleranceMs?: number; +}) => { + const customer = providedCustomer + ? providedCustomer + : await defaultAutumn.customers.get(customerId!); + + await expectCustomerProductCorrect({ + customer: customer as ApiCustomerV3, + productId, + state: "scheduled", + }); + + if (startsAt !== undefined) { + const products = customer.products ?? []; + const product = products.find((p: { id?: string }) => p.id === productId); + + if (!product) { + throw new Error(`Product ${productId} not found for startsAt check`); + } + + const actualStartsAt = product.started_at; + const diff = Math.abs(actualStartsAt - startsAt); + + expect( + diff <= toleranceMs, + `Product ${productId} started_at (${actualStartsAt}) should be within ${toleranceMs}ms of expected (${startsAt}), diff: ${diff}ms`, + ).toBe(true); + } +}; /** * Shorthand for checking product does not exist diff --git a/server/tests/interval/multiSub/multiSubInterval3.test.ts b/server/tests/interval/multiSub/multiSubInterval3.test.ts index 29b71eee3..0305256ce 100644 --- a/server/tests/interval/multiSub/multiSubInterval3.test.ts +++ b/server/tests/interval/multiSub/multiSubInterval3.test.ts @@ -1,11 +1,11 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { LegacyVersion } from "@autumn/shared"; -import chalk from "chalk"; -import { addMonths, addYears, differenceInDays } from "date-fns"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { advanceTestClock } from "@tests/utils/stripeUtils.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import { addMonths, addYears, differenceInDays } from "date-fns"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem, diff --git a/server/tests/merged/add/mergedAdd3.test.ts b/server/tests/merged/add/mergedAdd3.test.ts index 3b593fd36..6c282420b 100644 --- a/server/tests/merged/add/mergedAdd3.test.ts +++ b/server/tests/merged/add/mergedAdd3.test.ts @@ -5,12 +5,12 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/merged/downgrade/mergedDowngrade1.test.ts b/server/tests/merged/downgrade/mergedDowngrade1.test.ts index 668bee80e..23ad39a90 100644 --- a/server/tests/merged/downgrade/mergedDowngrade1.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade1.test.ts @@ -5,11 +5,11 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/merged/downgrade/mergedDowngrade6.test.ts b/server/tests/merged/downgrade/mergedDowngrade6.test.ts index 27e3aa3b1..1661e8349 100644 --- a/server/tests/merged/downgrade/mergedDowngrade6.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade6.test.ts @@ -5,12 +5,12 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/merged/downgrade/mergedDowngrade8.test.ts b/server/tests/merged/downgrade/mergedDowngrade8.test.ts index a20fd4720..86ac92346 100644 --- a/server/tests/merged/downgrade/mergedDowngrade8.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade8.test.ts @@ -5,11 +5,11 @@ import { LegacyVersion, type Organization, } from "@autumn/shared"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; import { TestFeature } from "@tests/setup/v2Features.js"; import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; diff --git a/server/tests/merged/separate/separate1.test.ts b/server/tests/merged/separate/separate1.test.ts index 131b96f70..82923b2cc 100644 --- a/server/tests/merged/separate/separate1.test.ts +++ b/server/tests/merged/separate/separate1.test.ts @@ -1,9 +1,9 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; -import chalk from "chalk"; import { TestFeature } from "@tests/setup/v2Features.js"; import { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout.js"; import ctx from "@tests/utils/testInitUtils/createTestContext.js"; +import chalk from "chalk"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; diff --git a/server/tests/unit/billing/stripe/discounts/sub-to-discounts.spec.ts b/server/tests/unit/billing/stripe/discounts/sub-to-discounts.spec.ts index 8c34b90d7..82ba43b17 100644 --- a/server/tests/unit/billing/stripe/discounts/sub-to-discounts.spec.ts +++ b/server/tests/unit/billing/stripe/discounts/sub-to-discounts.spec.ts @@ -98,7 +98,9 @@ describe(chalk.yellowBright("subToDiscounts"), () => { describe(chalk.cyan("Successful extraction"), () => { test("extracts single percent-off discount", () => { - const discount = discounts.twentyPercentOff({ couponId: "coupon_20_pct" }); + const discount = discounts.twentyPercentOff({ + couponId: "coupon_20_pct", + }); const sub = stripeSubscriptions.create({ id: "sub_test", discounts: [discount], diff --git a/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update-multi-product.spec.ts b/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update-multi-product.spec.ts index 4e3e7c456..00e63ec6c 100644 --- a/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update-multi-product.spec.ts +++ b/server/tests/unit/billing/stripe/subscriptions/build-subscription-items-update-multi-product.spec.ts @@ -12,9 +12,9 @@ import { describe, expect, test } from "bun:test"; import { CusProductStatus } from "@autumn/shared"; import { contexts } from "@tests/utils/fixtures/db/contexts"; import { customerProducts } from "@tests/utils/fixtures/db/customerProducts"; +import { stripeSubscriptions } from "@tests/utils/fixtures/stripe/subscriptions"; import chalk from "chalk"; import { buildStripeSubscriptionItemsUpdate } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; -import { stripeSubscriptions } from "@tests/utils/fixtures/stripe/subscriptions"; import { createCustomerPricesForProduct, createProductWithAllPriceTypes, diff --git a/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts b/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts index c9f6dafad..00ff0c6f5 100644 --- a/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts +++ b/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts @@ -1,11 +1,12 @@ -import { BillingType, EntInterval } from "@autumn/shared"; - -import { getBillingType } from "@/internal/products/prices/priceUtils.js"; - -import { FullCustomerEntitlement } from "@autumn/shared"; +import { + BillingType, + EntInterval, + type FullCusProduct, + type FullCustomerEntitlement, +} from "@autumn/shared"; import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; -import { FullCusProduct } from "@autumn/shared"; +import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { notNullish } from "@/utils/genUtils.js"; export const getLifetimeFreeCusEnt = ({ @@ -22,7 +23,7 @@ export const getLifetimeFreeCusEnt = ({ return false; } - let relatedCusPrice = getRelatedCusPrice( + const relatedCusPrice = getRelatedCusPrice( cusEnt, cusProduct.customer_prices, ); @@ -49,7 +50,7 @@ export const getPrepaidCusEnt = ({ return false; } - let relatedCusPrice = getRelatedCusPrice( + const relatedCusPrice = getRelatedCusPrice( cusEnt, cusProduct.customer_prices, ); @@ -81,7 +82,7 @@ export const getUsageCusEnt = ({ return false; } - let relatedCusPrice = getRelatedCusPrice( + const relatedCusPrice = getRelatedCusPrice( cusEnt, cusProduct.customer_prices, ); diff --git a/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts b/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts index a67fd5638..1906d2862 100644 --- a/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts +++ b/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts @@ -1,4 +1,4 @@ -import { Customer } from "autumn-js"; +import type { Customer } from "autumn-js"; import { expect } from "chai"; export const expectResetAtCorrect = ({ diff --git a/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts b/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts index 457e3d31e..5958fe208 100644 --- a/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts +++ b/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts @@ -1,4 +1,4 @@ -import { Customer } from "autumn-js"; +import type { Customer } from "autumn-js"; import { expect } from "chai"; export const expectTrialEndsAtCorrect = ({ @@ -8,19 +8,19 @@ export const expectTrialEndsAtCorrect = ({ cusBefore: Customer; cusAfter: Customer; }) => { - let productsBefore = cusBefore.products; - let productsAfter = cusAfter.products; + const productsBefore = cusBefore.products; + const productsAfter = cusAfter.products; for (const productBefore of productsBefore) { - // @ts-ignore - let trialEndsAtBefore = productBefore.trial_ends_at; + // @ts-expect-error + const trialEndsAtBefore = productBefore.trial_ends_at; if (!trialEndsAtBefore) { continue; } const productAfter = productsAfter.find((p) => p.id === productBefore.id); - // @ts-ignore + // @ts-expect-error expect(productAfter?.trial_ends_at).to.equal(trialEndsAtBefore); } }; diff --git a/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts b/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts index 9fee45961..36662928f 100644 --- a/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts +++ b/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts @@ -1,7 +1,7 @@ -import { Decimal } from "decimal.js"; -import { timeout } from "@/utils/genUtils.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { expect } from "chai"; +import { Decimal } from "decimal.js"; +import type { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; export const useEntityBalanceAndExpect = async ({ autumn, @@ -14,11 +14,11 @@ export const useEntityBalanceAndExpect = async ({ featureId: string; entityId: string; }) => { - let deduction = new Decimal(Math.random() * 400) + const deduction = new Decimal(Math.random() * 400) .toDecimalPlaces(5) .toNumber(); - let balanceBefore = await autumn.check({ + const balanceBefore = await autumn.check({ customer_id: customerId, feature_id: featureId, entity_id: entityId, @@ -32,13 +32,13 @@ export const useEntityBalanceAndExpect = async ({ }); await timeout(3000); - let balanceAfter = await autumn.check({ + const balanceAfter = await autumn.check({ customer_id: customerId, feature_id: featureId, entity_id: entityId, }); - let expectedBalance = new Decimal(balanceBefore.balance!) + const expectedBalance = new Decimal(balanceBefore.balance!) .sub(deduction) .toNumber(); diff --git a/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts b/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts index 5c9cd8d56..e56382493 100644 --- a/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts +++ b/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts @@ -1,9 +1,9 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { ProductV2, Organization, ProductItem } from "@autumn/shared"; +import { Organization, type ProductItem, type ProductV2 } from "@autumn/shared"; import { AppEnv } from "autumn-js"; import { expect } from "chai"; import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnInt } from "@/external/autumn/autumnCli.js"; export const attachNewContUseAndExpectCorrect = async ({ autumn, diff --git a/server/tests/utils/fixtures/db/contexts.ts b/server/tests/utils/fixtures/db/contexts.ts index 1f6cd0180..5d31f8de5 100644 --- a/server/tests/utils/fixtures/db/contexts.ts +++ b/server/tests/utils/fixtures/db/contexts.ts @@ -6,12 +6,12 @@ import { type FullCusProduct, type FullProduct, } from "@autumn/shared"; +import type Stripe from "stripe"; import { logger } from "@/external/logtail/logtailUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import { customers } from "./customers"; import { stripeCustomers } from "../stripe/customers"; -import type Stripe from "stripe"; +import { customers } from "./customers"; /** * Create an organization fixture diff --git a/server/tests/utils/fixtures/db/entities.ts b/server/tests/utils/fixtures/db/entities.ts index 507edc9ba..a98e23007 100644 --- a/server/tests/utils/fixtures/db/entities.ts +++ b/server/tests/utils/fixtures/db/entities.ts @@ -33,7 +33,3 @@ const create = ({ export const entities = { create, } as const; - - - - diff --git a/server/tests/utils/fixtures/db/rollovers.ts b/server/tests/utils/fixtures/db/rollovers.ts index 023a766f8..2f15ed8a8 100644 --- a/server/tests/utils/fixtures/db/rollovers.ts +++ b/server/tests/utils/fixtures/db/rollovers.ts @@ -1,7 +1,4 @@ -import type { - EntityRolloverBalance, - Rollover, -} from "@autumn/shared"; +import type { EntityRolloverBalance, Rollover } from "@autumn/shared"; /** * Create a rollover fixture @@ -36,4 +33,3 @@ const create = ({ export const rollovers = { create, } as const; - diff --git a/shared/api/balances/check/checkParams.ts b/shared/api/balances/check/checkParams.ts index cabcb8076..eba38438d 100644 --- a/shared/api/balances/check/checkParams.ts +++ b/shared/api/balances/check/checkParams.ts @@ -8,8 +8,7 @@ const checkDescriptions = { customer_id: "ID which you provided when creating the customer", product_id: "ID of the product to check access to. Required if feature_id is not provided.", - feature_id: - "ID of the feature to check access to.", + feature_id: "ID of the feature to check access to.", required_balance: "If you know the amount of the feature the end user is consuming in advance. If their balance is below this quantity, allowed will be false.", send_event: diff --git a/shared/api/balances/create/createBalanceParams.ts b/shared/api/balances/create/createBalanceParams.ts index fbb3afb8f..54a0967b3 100644 --- a/shared/api/balances/create/createBalanceParams.ts +++ b/shared/api/balances/create/createBalanceParams.ts @@ -17,14 +17,18 @@ export const CreateBalanceParamsSchema = z customer_id: z.string().describe(descriptions.customer_id), entity_id: z.string().optional().describe(descriptions.entity_id), - granted_balance: z.number().optional().describe(descriptions.granted_balance), + granted_balance: z + .number() + .optional() + .describe(descriptions.granted_balance), unlimited: z.boolean().optional().describe(descriptions.unlimited), reset: z .object({ interval: z.enum(ResetInterval), interval_count: z.number().optional(), }) - .optional().describe(descriptions.reset), + .optional() + .describe(descriptions.reset), expires_at: z.number().optional().describe(descriptions.expires_at), // Unix timestamp in milliseconds }) .refine((data) => { diff --git a/shared/api/core/checkProductModels.ts b/shared/api/core/checkProductModels.ts index 7c72a6d87..5f652a014 100644 --- a/shared/api/core/checkProductModels.ts +++ b/shared/api/core/checkProductModels.ts @@ -90,70 +90,69 @@ export const CheckProductPreviewOptionSchema = z.object({ }), }); -export const CheckProductPreviewSchema = z - .object({ - scenario: ProductScenarioSchema, - product_id: z.string().meta({ - description: "The ID of the product", - example: "pro_plan", - }), - product_name: z.string().meta({ - description: "The name of the product", - example: "Pro Plan", - }), - recurring: z.boolean().meta({ - description: "Whether the product is recurring", - example: true, - }), - error_on_attach: z.boolean().optional().meta({ - description: "Whether there would be an error attaching this product", - example: false, - }), - next_cycle_at: z.number().optional().meta({ - description: "Timestamp of the next billing cycle", - example: 1717000000000, - }), - current_product_name: z.string().optional().meta({ - description: "Name of the customer's current product", - example: "Basic Plan", - }), - items: z.array(CheckProductPreviewItemSchema).optional().meta({ - description: "Individual items in the product", - }), - options: z.array(CheckProductPreviewOptionSchema).optional().meta({ - description: "Feature options available in the product", - }), - due_today: z - .object({ - price: z.number().meta({ - description: "Amount due today", - example: 10, - }), - currency: z.string().meta({ - description: "Currency code", - example: "usd", - }), - }) - .optional() - .meta({ - description: "Payment due today", +export const CheckProductPreviewSchema = z.object({ + scenario: ProductScenarioSchema, + product_id: z.string().meta({ + description: "The ID of the product", + example: "pro_plan", + }), + product_name: z.string().meta({ + description: "The name of the product", + example: "Pro Plan", + }), + recurring: z.boolean().meta({ + description: "Whether the product is recurring", + example: true, + }), + error_on_attach: z.boolean().optional().meta({ + description: "Whether there would be an error attaching this product", + example: false, + }), + next_cycle_at: z.number().optional().meta({ + description: "Timestamp of the next billing cycle", + example: 1717000000000, + }), + current_product_name: z.string().optional().meta({ + description: "Name of the customer's current product", + example: "Basic Plan", + }), + items: z.array(CheckProductPreviewItemSchema).optional().meta({ + description: "Individual items in the product", + }), + options: z.array(CheckProductPreviewOptionSchema).optional().meta({ + description: "Feature options available in the product", + }), + due_today: z + .object({ + price: z.number().meta({ + description: "Amount due today", + example: 10, }), - due_next_cycle: z - .object({ - price: z.number().meta({ - description: "Amount due next cycle", - example: 50, - }), - currency: z.string().meta({ - description: "Currency code", - example: "usd", - }), - }) - .optional() - .meta({ - description: "Payment due in the next cycle", + currency: z.string().meta({ + description: "Currency code", + example: "usd", }), - }); + }) + .optional() + .meta({ + description: "Payment due today", + }), + due_next_cycle: z + .object({ + price: z.number().meta({ + description: "Amount due next cycle", + example: 50, + }), + currency: z.string().meta({ + description: "Currency code", + example: "usd", + }), + }) + .optional() + .meta({ + description: "Payment due in the next cycle", + }), +}); export const CheckProductResultSchema = z.object({ allowed: z.boolean().meta({ diff --git a/shared/api/features/featureV1OpModels.ts b/shared/api/features/featureV1OpModels.ts index 20ba946f4..f9dd2d70b 100644 --- a/shared/api/features/featureV1OpModels.ts +++ b/shared/api/features/featureV1OpModels.ts @@ -16,57 +16,55 @@ const featureDescriptions = { "Whether the feature is archived. Archived features are hidden from the dashboard and list features endpoint.", }; -export const BaseFeatureV1ParamsSchema = z - .object({ - id: z - .string() - .nonempty() - .regex(idRegex) - .meta({ description: featureDescriptions.id }), - name: z - .string() - .nonempty() - .nullish() - .meta({ description: featureDescriptions.name }), - type: z.enum(FeatureType).meta({ description: featureDescriptions.type }), - consumable: z.boolean().optional(), +export const BaseFeatureV1ParamsSchema = z.object({ + id: z + .string() + .nonempty() + .regex(idRegex) + .meta({ description: featureDescriptions.id }), + name: z + .string() + .nonempty() + .nullish() + .meta({ description: featureDescriptions.name }), + type: z.enum(FeatureType).meta({ description: featureDescriptions.type }), + consumable: z.boolean().optional(), - display: z - .object({ - singular: z.string(), - plural: z.string(), - }) - .optional() - .meta({ description: featureDescriptions.display }), + display: z + .object({ + singular: z.string(), + plural: z.string(), + }) + .optional() + .meta({ description: featureDescriptions.display }), - credit_schema: z - .array( - z.object({ - metered_feature_id: z.string(), - credit_cost: z.number(), - }), - ) - .optional() - .meta({ description: featureDescriptions.credit_schema }), + credit_schema: z + .array( + z.object({ + metered_feature_id: z.string(), + credit_cost: z.number(), + }), + ) + .optional() + .meta({ description: featureDescriptions.credit_schema }), - event_names: z.array(z.string()).optional(), - }); + event_names: z.array(z.string()).optional(), +}); // Create Feature Params -export const CreateFeatureV1ParamsSchema = BaseFeatureV1ParamsSchema - .refine( - (data: z.infer) => { - if (data.type === FeatureType.Metered && nullish(data.consumable)) { - return false; - } - return true; - }, - { - message: - "Please specify whether the feature is consumable (eg. API tokens, credits, etc.) or not.", - path: ["consumable"], - }, - ) +export const CreateFeatureV1ParamsSchema = BaseFeatureV1ParamsSchema.refine( + (data: z.infer) => { + if (data.type === FeatureType.Metered && nullish(data.consumable)) { + return false; + } + return true; + }, + { + message: + "Please specify whether the feature is consumable (eg. API tokens, credits, etc.) or not.", + path: ["consumable"], + }, +) .refine( (data) => { if ( diff --git a/shared/api/others/apiDiscount.ts b/shared/api/others/apiDiscount.ts index 698cde892..67515c52d 100644 --- a/shared/api/others/apiDiscount.ts +++ b/shared/api/others/apiDiscount.ts @@ -58,7 +58,14 @@ export const ApiDiscountSchema = z.object({ export const ApiCusRewardsSchema = z.object({ discounts: z.array(ApiDiscountSchema).meta({ description: "Array of active discounts applied to the customer", - example: [{ id: "disc_123456", name: "SUMMER20", type: "percentage", discount_value: 20 }], + example: [ + { + id: "disc_123456", + name: "SUMMER20", + type: "percentage", + discount_value: 20, + }, + ], }), }); diff --git a/shared/db/utils.ts b/shared/db/utils.ts index 099ab6798..5153400a7 100644 --- a/shared/db/utils.ts +++ b/shared/db/utils.ts @@ -1,5 +1,6 @@ -import { PgColumn } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; +import type { PgColumn } from "drizzle-orm/pg-core"; + function collatePgColumn>(col: C, collation: string) { const originalGetSQLType = col.getSQLType; col.getSQLType = function (this: C) { diff --git a/shared/models/analyticsModels/actionTable.ts b/shared/models/analyticsModels/actionTable.ts index 5d87661b7..771e20f2e 100644 --- a/shared/models/analyticsModels/actionTable.ts +++ b/shared/models/analyticsModels/actionTable.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { foreignKey, jsonb, @@ -5,12 +6,11 @@ import { text, timestamp, } from "drizzle-orm/pg-core"; -import { sql } from "drizzle-orm"; -import { ActionType, AuthType } from "./actionEnums.js"; -import { organizations } from "../orgModels/orgTable.js"; +import { collatePgColumn } from "../../db/utils.js"; import { customers } from "../cusModels/cusTable.js"; import { entities } from "../cusModels/entityModels/entityTable.js"; -import { collatePgColumn } from "../../db/utils.js"; +import { organizations } from "../orgModels/orgTable.js"; +import type { ActionType, AuthType } from "./actionEnums.js"; export const actions = pgTable( "actions", diff --git a/shared/models/attachModels/attachEnums/AttachConfig.ts b/shared/models/attachModels/attachEnums/AttachConfig.ts index 505a4747c..bdbf871ce 100644 --- a/shared/models/attachModels/attachEnums/AttachConfig.ts +++ b/shared/models/attachModels/attachEnums/AttachConfig.ts @@ -1,4 +1,4 @@ -import { AttachBranch } from "./AttachBranch.js"; +import type { AttachBranch } from "./AttachBranch.js"; export enum ProrationBehavior { Immediately = "immediately", diff --git a/shared/models/authModels/membership.ts b/shared/models/authModels/membership.ts index 3b62eb667..4b52acb8a 100644 --- a/shared/models/authModels/membership.ts +++ b/shared/models/authModels/membership.ts @@ -1,4 +1,4 @@ -import { Member, User } from "../../db/auth-schema.js"; +import type { Member, User } from "../../db/auth-schema.js"; export interface Membership { user: User; diff --git a/shared/models/chatResultModels/chatResultTable.ts b/shared/models/chatResultModels/chatResultTable.ts index 5d93878b5..a9737c836 100644 --- a/shared/models/chatResultModels/chatResultTable.ts +++ b/shared/models/chatResultModels/chatResultTable.ts @@ -1,8 +1,7 @@ -import { pgTable, text, numeric, jsonb, boolean } from "drizzle-orm/pg-core"; - -import { ChatResultFeature } from "../chatResultModels/chatResultFeature.js"; -import { ProductV2 } from "../productV2Models/productV2Models.js"; +import { boolean, jsonb, numeric, pgTable, text } from "drizzle-orm/pg-core"; import { collatePgColumn } from "../../db/utils.js"; +import type { ChatResultFeature } from "../chatResultModels/chatResultFeature.js"; +import type { ProductV2 } from "../productV2Models/productV2Models.js"; export const chatResults = pgTable("chat_results", { id: text("id").primaryKey(), diff --git a/shared/models/cusModels/cusRelations.ts b/shared/models/cusModels/cusRelations.ts index e5a757335..299423c68 100644 --- a/shared/models/cusModels/cusRelations.ts +++ b/shared/models/cusModels/cusRelations.ts @@ -1,8 +1,8 @@ import { relations } from "drizzle-orm"; -import { customers } from "./cusTable.js"; import { customerProducts } from "../cusProductModels/cusProductTable.js"; -import { entities } from "./entityModels/entityTable.js"; import { organizations } from "../orgModels/orgTable.js"; +import { customers } from "./cusTable.js"; +import { entities } from "./entityModels/entityTable.js"; export const customersRelations = relations(customers, ({ one, many }) => ({ customer_products: many(customerProducts), diff --git a/shared/models/cusModels/entityModels/entityRelations.ts b/shared/models/cusModels/entityModels/entityRelations.ts index 6117f32f5..bbc63fe96 100644 --- a/shared/models/cusModels/entityModels/entityRelations.ts +++ b/shared/models/cusModels/entityModels/entityRelations.ts @@ -1,8 +1,8 @@ import { relations } from "drizzle-orm"; -import { entities } from "./entityTable.js"; -import { customers } from "../cusTable.js"; import { features } from "../../featureModels/featureTable.js"; import { organizations } from "../../orgModels/orgTable.js"; +import { customers } from "../cusTable.js"; +import { entities } from "./entityTable.js"; export const entitiesRelations = relations(entities, ({ one }) => ({ customer: one(customers, { diff --git a/shared/models/cusModels/invoiceModels/invoiceRelations.ts b/shared/models/cusModels/invoiceModels/invoiceRelations.ts index 944d50fa5..038c365ec 100644 --- a/shared/models/cusModels/invoiceModels/invoiceRelations.ts +++ b/shared/models/cusModels/invoiceModels/invoiceRelations.ts @@ -1,6 +1,6 @@ +import { relations } from "drizzle-orm"; import { customers } from "../cusTable.js"; import { invoices } from "./invoiceTable.js"; -import { relations } from "drizzle-orm"; export const invoiceRelations = relations(invoices, ({ one }) => ({ customer: one(customers, { diff --git a/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts b/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts index f4052536d..af85275f9 100644 --- a/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts +++ b/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts @@ -1,6 +1,6 @@ import { relations } from "drizzle-orm"; -import { replaceables } from "./replaceableTable.js"; import { customerEntitlements } from "./cusEntTable.js"; +import { replaceables } from "./replaceableTable.js"; export const replaceableRelations = relations(replaceables, ({ one }) => ({ customer_entitlement: one(customerEntitlements, { diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts index 51257521b..6492d0f41 100644 --- a/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts @@ -2,8 +2,8 @@ import { relations } from "drizzle-orm"; import { customers } from "../../cusModels/cusTable.js"; import { prices } from "../../productModels/priceModels/priceTable.js"; -import { customerPrices } from "./cusPriceTable.js"; import { customerProducts } from "../cusProductTable.js"; +import { customerPrices } from "./cusPriceTable.js"; export const customerPricesRelations = relations( customerPrices, diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts index 47f49dafb..22bcba0a5 100644 --- a/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts @@ -1,5 +1,11 @@ -import { foreignKey, index, jsonb, text } from "drizzle-orm/pg-core"; -import { numeric, pgTable } from "drizzle-orm/pg-core"; +import { + foreignKey, + index, + jsonb, + numeric, + pgTable, + text, +} from "drizzle-orm/pg-core"; import { customers } from "../../cusModels/cusTable.js"; import { prices } from "../../productModels/priceModels/priceTable.js"; diff --git a/shared/models/cusProductModels/cusProductRelations.ts b/shared/models/cusProductModels/cusProductRelations.ts index a924679d0..ea82246c8 100644 --- a/shared/models/cusProductModels/cusProductRelations.ts +++ b/shared/models/cusProductModels/cusProductRelations.ts @@ -1,10 +1,10 @@ import { relations } from "drizzle-orm"; -import { customerProducts } from "./cusProductTable.js"; import { customers } from "../cusModels/cusTable.js"; -import { products } from "../productModels/productTable.js"; import { freeTrials } from "../productModels/freeTrialModels/freeTrialTable.js"; +import { products } from "../productModels/productTable.js"; import { customerEntitlements } from "./cusEntModels/cusEntTable.js"; import { customerPrices } from "./cusPriceModels/cusPriceTable.js"; +import { customerProducts } from "./cusProductTable.js"; export const customerProductsRelations = relations( customerProducts, diff --git a/shared/models/devModels/apiKeyModels.ts b/shared/models/devModels/apiKeyModels.ts index 103ceb5d1..d97f1a10b 100644 --- a/shared/models/devModels/apiKeyModels.ts +++ b/shared/models/devModels/apiKeyModels.ts @@ -1,4 +1,4 @@ -import { AppEnv } from "../genModels/genEnums.js"; +import type { AppEnv } from "../genModels/genEnums.js"; export type ApiKey = { id: string; diff --git a/shared/models/devModels/apiKeyTable.ts b/shared/models/devModels/apiKeyTable.ts index 8466afeeb..1ad365b3f 100644 --- a/shared/models/devModels/apiKeyTable.ts +++ b/shared/models/devModels/apiKeyTable.ts @@ -1,14 +1,14 @@ import { + foreignKey, + index, + jsonb, + numeric, pgTable, text, - numeric, - jsonb, - foreignKey, unique, - index, } from "drizzle-orm/pg-core"; -import { organizations } from "../orgModels/orgTable.js"; import { collatePgColumn, sqlNow } from "../../db/utils.js"; +import { organizations } from "../orgModels/orgTable.js"; export const apiKeys = pgTable( "api_keys", diff --git a/shared/models/eventModels/eventTable.ts b/shared/models/eventModels/eventTable.ts index 9d9c2cfc9..64b5beab4 100644 --- a/shared/models/eventModels/eventTable.ts +++ b/shared/models/eventModels/eventTable.ts @@ -1,15 +1,15 @@ import { sql } from "drizzle-orm"; import { + bigint, + boolean, + foreignKey, + index, + jsonb, + numeric, pgTable, text, - numeric, - boolean, - jsonb, - foreignKey, - unique, - bigint, timestamp, - index, + unique, } from "drizzle-orm/pg-core"; import { customers } from "../cusModels/cusTable.js"; diff --git a/shared/models/migrationModels/migrationErrorRelations.ts b/shared/models/migrationModels/migrationErrorRelations.ts index 3bf12e978..c700bb7ab 100644 --- a/shared/models/migrationModels/migrationErrorRelations.ts +++ b/shared/models/migrationModels/migrationErrorRelations.ts @@ -1,6 +1,6 @@ import { relations } from "drizzle-orm"; -import { migrationErrors } from "./migrationErrorTable.js"; import { customers } from "../cusModels/cusTable.js"; +import { migrationErrors } from "./migrationErrorTable.js"; export const migrationErrorRelations = relations( migrationErrors, diff --git a/shared/models/migrationModels/migrationJobTable.ts b/shared/models/migrationModels/migrationJobTable.ts index 118b64270..f0f29f644 100644 --- a/shared/models/migrationModels/migrationJobTable.ts +++ b/shared/models/migrationModels/migrationJobTable.ts @@ -1,6 +1,6 @@ -import { pgTable, text, numeric, jsonb, foreignKey } from "drizzle-orm/pg-core"; -import { products } from "../productModels/productTable.js"; +import { foreignKey, jsonb, numeric, pgTable, text } from "drizzle-orm/pg-core"; import { organizations } from "../orgModels/orgTable.js"; +import { products } from "../productModels/productTable.js"; export const migrationJobs = pgTable( "migration_jobs", diff --git a/shared/models/productModels/entModels/entRelations.ts b/shared/models/productModels/entModels/entRelations.ts index ac160d5cc..87387887a 100644 --- a/shared/models/productModels/entModels/entRelations.ts +++ b/shared/models/productModels/entModels/entRelations.ts @@ -1,8 +1,8 @@ import { relations } from "drizzle-orm"; import { features } from "../../featureModels/featureTable.js"; -import { entitlements } from "./entTable.js"; import { products } from "../productTable.js"; +import { entitlements } from "./entTable.js"; export const entitlementsRelations = relations(entitlements, ({ one }) => ({ feature: one(features, { diff --git a/shared/models/productModels/freeTrialModels/freeTrialRelations.ts b/shared/models/productModels/freeTrialModels/freeTrialRelations.ts index 4e681f24a..542a3841a 100644 --- a/shared/models/productModels/freeTrialModels/freeTrialRelations.ts +++ b/shared/models/productModels/freeTrialModels/freeTrialRelations.ts @@ -1,6 +1,6 @@ import { relations } from "drizzle-orm"; -import { freeTrials } from "./freeTrialTable.js"; import { products } from "../productTable.js"; +import { freeTrials } from "./freeTrialTable.js"; export const freeTrialRelations = relations(freeTrials, ({ one }) => ({ product: one(products, { diff --git a/shared/models/productModels/priceModels/priceRelations.ts b/shared/models/productModels/priceModels/priceRelations.ts index 4adbb2f9e..6e99db9d8 100644 --- a/shared/models/productModels/priceModels/priceRelations.ts +++ b/shared/models/productModels/priceModels/priceRelations.ts @@ -1,6 +1,6 @@ import { relations } from "drizzle-orm"; -import { prices } from "./priceTable.js"; import { products } from "../productTable.js"; +import { prices } from "./priceTable.js"; export const priceRelations = relations(prices, ({ one }) => ({ product: one(products, { diff --git a/shared/models/productModels/priceModels/priceTable.ts b/shared/models/productModels/priceModels/priceTable.ts index 12afe21b0..84547cf04 100644 --- a/shared/models/productModels/priceModels/priceTable.ts +++ b/shared/models/productModels/priceModels/priceTable.ts @@ -1,21 +1,20 @@ +import { sql } from "drizzle-orm"; import { boolean, foreignKey, - unique, - pgTable, - numeric, - jsonb, - text, index, + jsonb, + numeric, + pgTable, + text, + unique, } from "drizzle-orm/pg-core"; - +import { collatePgColumn } from "../../../db/utils.js"; import { entitlements } from "../entModels/entTable.js"; import { products } from "../productTable.js"; -import { FixedPriceConfig } from "./priceConfig/fixedPriceConfig.js"; -import { UsagePriceConfig } from "./priceConfig/usagePriceConfig.js"; -import { sql } from "drizzle-orm"; -import { collatePgColumn } from "../../../db/utils.js"; -import { ProrationConfig } from "./priceModels.js"; +import type { FixedPriceConfig } from "./priceConfig/fixedPriceConfig.js"; +import type { UsagePriceConfig } from "./priceConfig/usagePriceConfig.js"; +import type { ProrationConfig } from "./priceModels.js"; export const prices = pgTable( "prices", diff --git a/shared/models/rewardModels/referralModels/referralCodeRelations.ts b/shared/models/rewardModels/referralModels/referralCodeRelations.ts index f1ddc352f..5b0fc9874 100644 --- a/shared/models/rewardModels/referralModels/referralCodeRelations.ts +++ b/shared/models/rewardModels/referralModels/referralCodeRelations.ts @@ -1,7 +1,7 @@ import { relations } from "drizzle-orm"; -import { referralCodes } from "./referralCodeTable.js"; import { customers } from "../../cusModels/cusTable.js"; import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; +import { referralCodes } from "./referralCodeTable.js"; import { rewardRedemptions } from "./rewardRedemptionTable.js"; export const referralCodeRelations = relations( diff --git a/shared/models/rewardModels/referralModels/referralCodeTable.ts b/shared/models/rewardModels/referralModels/referralCodeTable.ts index 8b93ded6e..1af0d6d31 100644 --- a/shared/models/rewardModels/referralModels/referralCodeTable.ts +++ b/shared/models/rewardModels/referralModels/referralCodeTable.ts @@ -1,14 +1,14 @@ import { - text, foreignKey, - pgTable, numeric, + pgTable, primaryKey, + text, unique, } from "drizzle-orm/pg-core"; import { customers } from "../../cusModels/cusTable.js"; -import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; import { organizations } from "../../orgModels/orgTable.js"; +import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; export const referralCodes = pgTable( "referral_codes", diff --git a/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts b/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts index 4ed8c489e..48e99d2b4 100644 --- a/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts +++ b/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts @@ -1,8 +1,8 @@ import { relations } from "drizzle-orm"; -import { rewardRedemptions } from "./rewardRedemptionTable.js"; import { customers } from "../../cusModels/cusTable.js"; -import { referralCodes } from "./referralCodeTable.js"; import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; +import { referralCodes } from "./referralCodeTable.js"; +import { rewardRedemptions } from "./rewardRedemptionTable.js"; export const rewardRedemptionRelations = relations( rewardRedemptions, diff --git a/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts b/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts index fef25e076..f5a8917b8 100644 --- a/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts +++ b/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts @@ -1,9 +1,9 @@ import { - text, - foreignKey, boolean, - pgTable, + foreignKey, numeric, + pgTable, + text, } from "drizzle-orm/pg-core"; import { customers } from "../../cusModels/cusTable.js"; import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts index 7e4633759..6f2ef5da5 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts @@ -1,6 +1,6 @@ import { relations } from "drizzle-orm"; -import { rewardPrograms } from "./rewardProgramTable.js"; import { rewards } from "../rewardModels/rewardTable.js"; +import { rewardPrograms } from "./rewardProgramTable.js"; export const rewardProgramRelations = relations(rewardPrograms, ({ one }) => ({ reward: one(rewards, { diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts index 4e9d3eebe..e000ab5ad 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts @@ -1,8 +1,13 @@ -import { pgTable, text, foreignKey, boolean } from "drizzle-orm/pg-core"; -import { numeric } from "drizzle-orm/pg-core"; +import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; +import { + boolean, + foreignKey, + numeric, + pgTable, + text, +} from "drizzle-orm/pg-core"; import { organizations } from "../../orgModels/orgTable.js"; import { rewards } from "../rewardModels/rewardTable.js"; -import { InferSelectModel, InferInsertModel } from "drizzle-orm"; export const rewardPrograms = pgTable( "reward_programs", diff --git a/shared/models/subModels/subTable.ts b/shared/models/subModels/subTable.ts index 456c4344c..31b335b95 100644 --- a/shared/models/subModels/subTable.ts +++ b/shared/models/subModels/subTable.ts @@ -1,14 +1,14 @@ +import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; import { + foreignKey, + jsonb, + numeric, pgTable, text, - numeric, - jsonb, - foreignKey, unique, } from "drizzle-orm/pg-core"; -import { organizations } from "../orgModels/orgTable.js"; import { sqlNow } from "../../db/utils.js"; -import { InferSelectModel, InferInsertModel } from "drizzle-orm"; +import { organizations } from "../orgModels/orgTable.js"; export const subscriptions = pgTable( "subscriptions", diff --git a/shared/utils/billingUtils/invoicingUtils/lineItemUtils/priceToLineAmount.ts b/shared/utils/billingUtils/invoicingUtils/lineItemUtils/priceToLineAmount.ts index 95a6b4f01..8f3ea798d 100644 --- a/shared/utils/billingUtils/invoicingUtils/lineItemUtils/priceToLineAmount.ts +++ b/shared/utils/billingUtils/invoicingUtils/lineItemUtils/priceToLineAmount.ts @@ -1,8 +1,8 @@ import { - isFixedPrice, - nullish, - type Price, - tiersToLineAmount, + isFixedPrice, + nullish, + type Price, + tiersToLineAmount, } from "@autumn/shared"; import { Decimal } from "decimal.js"; /** @@ -14,30 +14,30 @@ import { Decimal } from "decimal.js"; */ export const priceToLineAmount = ({ - price, - overage, - multiplier = 1, + price, + overage, + multiplier = 1, }: { - price: Price; - overage?: number; - multiplier?: number; + price: Price; + overage?: number; + multiplier?: number; }): number => { - // Fixed prices: flat amount × multiplier - if (isFixedPrice(price)) { - const config = price.config; - return new Decimal(config.amount).mul(multiplier).toNumber(); - } + // Fixed prices: flat amount × multiplier + if (isFixedPrice(price)) { + const config = price.config; + return new Decimal(config.amount).mul(multiplier).toNumber(); + } - // Usage-based prices: tiered calculation - if (nullish(overage)) { - throw new Error( - `[priceToLineAmount] overage required for usage-based prices`, - ); - } + // Usage-based prices: tiered calculation + if (nullish(overage)) { + throw new Error( + `[priceToLineAmount] overage required for usage-based prices`, + ); + } - return tiersToLineAmount({ - price, - overage, - billingUnits: price.config.billing_units ?? 1, - }); + return tiersToLineAmount({ + price, + overage, + billingUnits: price.config.billing_units ?? 1, + }); }; diff --git a/shared/utils/billingUtils/invoicingUtils/prorationUtils/applyProrationToLineItem.ts b/shared/utils/billingUtils/invoicingUtils/prorationUtils/applyProrationToLineItem.ts index 8b1378917..e69de29bb 100644 --- a/shared/utils/billingUtils/invoicingUtils/prorationUtils/applyProrationToLineItem.ts +++ b/shared/utils/billingUtils/invoicingUtils/prorationUtils/applyProrationToLineItem.ts @@ -1 +0,0 @@ - diff --git a/shared/utils/billingUtils/invoicingUtils/prorationUtils/prorationConfigUtils.ts b/shared/utils/billingUtils/invoicingUtils/prorationUtils/prorationConfigUtils.ts index 16f23089c..60031fef9 100644 --- a/shared/utils/billingUtils/invoicingUtils/prorationUtils/prorationConfigUtils.ts +++ b/shared/utils/billingUtils/invoicingUtils/prorationUtils/prorationConfigUtils.ts @@ -82,5 +82,5 @@ export const shouldSkipLineItems = ( prorationConfig === OnDecrease.NoProrations || prorationConfig === OnDecrease.None || prorationConfig === OnIncrease.BillNextCycle - ) + ); }; diff --git a/shared/utils/cusEntUtils/convertCusEntUtils.ts b/shared/utils/cusEntUtils/convertCusEntUtils.ts index b14d5e2d3..6bd303c85 100644 --- a/shared/utils/cusEntUtils/convertCusEntUtils.ts +++ b/shared/utils/cusEntUtils/convertCusEntUtils.ts @@ -12,7 +12,7 @@ import { getStartingBalance } from "./getStartingBalance.js"; export const cusEntsToPlanId = ({ cusEnts, }: { - cusEnts: (FullCusEntWithFullCusProduct)[]; + cusEnts: FullCusEntWithFullCusProduct[]; }) => { // Get number of keys const uniquePlanIds = new Set(); diff --git a/shared/utils/cusEntUtils/findCustomerEntitlement/findCustomerEntitlementByFeature.ts b/shared/utils/cusEntUtils/findCustomerEntitlement/findCustomerEntitlementByFeature.ts index 8e7277556..9d78846eb 100644 --- a/shared/utils/cusEntUtils/findCustomerEntitlement/findCustomerEntitlementByFeature.ts +++ b/shared/utils/cusEntUtils/findCustomerEntitlement/findCustomerEntitlementByFeature.ts @@ -1,7 +1,7 @@ import { InternalError } from "@autumn/shared"; -import type { Feature } from "@models/featureModels/featureModels"; import type { FullCustomerEntitlement } from "@models/cusProductModels/cusEntModels/cusEntModels"; import type { FullCusEntWithFullCusProduct } from "@models/cusProductModels/cusEntModels/cusEntWithProduct"; +import type { Feature } from "@models/featureModels/featureModels"; export function findCustomerEntitlementByFeature< T extends FullCustomerEntitlement | FullCusEntWithFullCusProduct, @@ -44,8 +44,7 @@ export function findCustomerEntitlementByFeature< } if ( featureId && - (ce.entitlement.feature_id === featureId || - ce.feature_id === featureId) + (ce.entitlement.feature_id === featureId || ce.feature_id === featureId) ) { return true; } diff --git a/shared/utils/cusPriceUtils/index.ts b/shared/utils/cusPriceUtils/index.ts index 1026a72fe..87cf6045f 100644 --- a/shared/utils/cusPriceUtils/index.ts +++ b/shared/utils/cusPriceUtils/index.ts @@ -1,6 +1,5 @@ export * from "./convertCusPriceUtils.js"; export * from "./convertCustomerPrice/customerPriceToBillingUnits.js"; export * from "./convertCustomerPrice/customerPriceToCustomerEntitlement.js"; -export * from "./findCusPriceUtils.js"; export * from "./findCusPriceUtils/findPrepaidCusPriceByFeature.js"; - +export * from "./findCusPriceUtils.js"; diff --git a/shared/utils/featureUtils/convertFeatureUtils.ts b/shared/utils/featureUtils/convertFeatureUtils.ts index bd1a6ba1f..26ad0be5d 100644 --- a/shared/utils/featureUtils/convertFeatureUtils.ts +++ b/shared/utils/featureUtils/convertFeatureUtils.ts @@ -5,7 +5,7 @@ import { import type { Feature } from "@models/featureModels/featureModels.js"; import { ProductItemFeatureType } from "@models/productV2Models/productItemModels/productItemModels.js"; import { ApiFeatureType } from "../../api/models.js"; -import { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js"; +import type { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js"; export const featureToItemFeatureType = ({ feature }: { feature: Feature }) => { let featureType: ProductItemFeatureType; @@ -60,13 +60,21 @@ export const isBooleanFeature = ({ feature }: { feature: Feature }) => { return feature.type === FeatureType.Boolean; }; -export const featureToOptions = ({ feature, options }: { feature: Feature, options: FeatureOptions[] }) => { +export const featureToOptions = ({ + feature, + options, +}: { + feature: Feature; + options: FeatureOptions[]; +}) => { // Check if options has internal feature id - const option = options.find(o => o.internal_feature_id === feature.internal_id); + const option = options.find( + (o) => o.internal_feature_id === feature.internal_id, + ); if (option) return option; - const option2 = options.find(o => o.feature_id === feature.id); + const option2 = options.find((o) => o.feature_id === feature.id); if (option2) return option2; return; -}; \ No newline at end of file +}; diff --git a/vite/eslint.config.js b/vite/eslint.config.js index 3899db104..6a7032dba 100644 --- a/vite/eslint.config.js +++ b/vite/eslint.config.js @@ -1,7 +1,7 @@ import js from "@eslint/js"; -import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; import tseslint from "typescript-eslint"; export default tseslint.config( diff --git a/vite/src/components/ai-elements/artifact.tsx b/vite/src/components/ai-elements/artifact.tsx index c90cb5fe3..5bfd0a6a1 100644 --- a/vite/src/components/ai-elements/artifact.tsx +++ b/vite/src/components/ai-elements/artifact.tsx @@ -1,147 +1,147 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; import { type LucideIcon, XIcon } from "lucide-react"; import type { ComponentProps, HTMLAttributes } from "react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; export type ArtifactProps = HTMLAttributes; export const Artifact = ({ className, ...props }: ArtifactProps) => ( -
+
); export type ArtifactHeaderProps = HTMLAttributes; export const ArtifactHeader = ({ - className, - ...props + className, + ...props }: ArtifactHeaderProps) => ( -
+
); export type ArtifactCloseProps = ComponentProps; export const ArtifactClose = ({ - className, - children, - size = "sm", - variant = "ghost", - ...props + className, + children, + size = "sm", + variant = "ghost", + ...props }: ArtifactCloseProps) => ( - + ); export type ArtifactTitleProps = HTMLAttributes; export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => ( -

+

); export type ArtifactDescriptionProps = HTMLAttributes; export const ArtifactDescription = ({ - className, - ...props + className, + ...props }: ArtifactDescriptionProps) => ( -

+

); export type ArtifactActionsProps = HTMLAttributes; export const ArtifactActions = ({ - className, - ...props + className, + ...props }: ArtifactActionsProps) => ( -

+
); export type ArtifactActionProps = ComponentProps & { - tooltip?: string; - label?: string; - icon?: LucideIcon; + tooltip?: string; + label?: string; + icon?: LucideIcon; }; export const ArtifactAction = ({ - tooltip, - label, - icon: Icon, - children, - className, - size = "sm", - variant = "ghost", - ...props + tooltip, + label, + icon: Icon, + children, + className, + size = "sm", + variant = "ghost", + ...props }: ArtifactActionProps) => { - const button = ( - - ); + const button = ( + + ); - if (tooltip) { - return ( - - - {button} - -

{tooltip}

-
-
-
- ); - } + if (tooltip) { + return ( + + + {button} + +

{tooltip}

+
+
+
+ ); + } - return button; + return button; }; export type ArtifactContentProps = HTMLAttributes; export const ArtifactContent = ({ - className, - ...props + className, + ...props }: ArtifactContentProps) => ( -
+
); diff --git a/vite/src/components/ai-elements/canvas.tsx b/vite/src/components/ai-elements/canvas.tsx index 5aa83cb5e..05e66d648 100644 --- a/vite/src/components/ai-elements/canvas.tsx +++ b/vite/src/components/ai-elements/canvas.tsx @@ -3,20 +3,20 @@ import type { ReactNode } from "react"; import "@xyflow/react/dist/style.css"; type CanvasProps = ReactFlowProps & { - children?: ReactNode; + children?: ReactNode; }; export const Canvas = ({ children, ...props }: CanvasProps) => ( - - - {children} - + + + {children} + ); diff --git a/vite/src/components/ai-elements/chain-of-thought.tsx b/vite/src/components/ai-elements/chain-of-thought.tsx index 195c465c7..f2d1dca4a 100644 --- a/vite/src/components/ai-elements/chain-of-thought.tsx +++ b/vite/src/components/ai-elements/chain-of-thought.tsx @@ -1,225 +1,225 @@ "use client"; import { useControllableState } from "@radix-ui/react-use-controllable-state"; -import { Badge } from "@/components/ui/badge"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; -import { - BrainIcon, - ChevronDownIcon, - DotIcon, - type LucideIcon, + BrainIcon, + ChevronDownIcon, + DotIcon, + type LucideIcon, } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { createContext, memo, useContext, useMemo } from "react"; +import { Badge } from "@/components/ui/badge"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; type ChainOfThoughtContextValue = { - isOpen: boolean; - setIsOpen: (open: boolean) => void; + isOpen: boolean; + setIsOpen: (open: boolean) => void; }; const ChainOfThoughtContext = createContext( - null + null, ); const useChainOfThought = () => { - const context = useContext(ChainOfThoughtContext); - if (!context) { - throw new Error( - "ChainOfThought components must be used within ChainOfThought" - ); - } - return context; + const context = useContext(ChainOfThoughtContext); + if (!context) { + throw new Error( + "ChainOfThought components must be used within ChainOfThought", + ); + } + return context; }; export type ChainOfThoughtProps = ComponentProps<"div"> & { - open?: boolean; - defaultOpen?: boolean; - onOpenChange?: (open: boolean) => void; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; }; export const ChainOfThought = memo( - ({ - className, - open, - defaultOpen = false, - onOpenChange, - children, - ...props - }: ChainOfThoughtProps) => { - const [isOpen, setIsOpen] = useControllableState({ - prop: open, - defaultProp: defaultOpen, - onChange: onOpenChange, - }); + ({ + className, + open, + defaultOpen = false, + onOpenChange, + children, + ...props + }: ChainOfThoughtProps) => { + const [isOpen, setIsOpen] = useControllableState({ + prop: open, + defaultProp: defaultOpen, + onChange: onOpenChange, + }); - const chainOfThoughtContext = useMemo( - () => ({ isOpen, setIsOpen }), - [isOpen, setIsOpen] - ); + const chainOfThoughtContext = useMemo( + () => ({ isOpen, setIsOpen }), + [isOpen, setIsOpen], + ); - return ( - -
- {children} -
-
- ); - } + return ( + +
+ {children} +
+
+ ); + }, ); export type ChainOfThoughtHeaderProps = ComponentProps< - typeof CollapsibleTrigger + typeof CollapsibleTrigger >; export const ChainOfThoughtHeader = memo( - ({ className, children, ...props }: ChainOfThoughtHeaderProps) => { - const { isOpen, setIsOpen } = useChainOfThought(); + ({ className, children, ...props }: ChainOfThoughtHeaderProps) => { + const { isOpen, setIsOpen } = useChainOfThought(); - return ( - - - - - {children ?? "Chain of Thought"} - - - - - ); - } + return ( + + + + + {children ?? "Chain of Thought"} + + + + + ); + }, ); export type ChainOfThoughtStepProps = ComponentProps<"div"> & { - icon?: LucideIcon; - label: ReactNode; - description?: ReactNode; - status?: "complete" | "active" | "pending"; + icon?: LucideIcon; + label: ReactNode; + description?: ReactNode; + status?: "complete" | "active" | "pending"; }; export const ChainOfThoughtStep = memo( - ({ - className, - icon: Icon = DotIcon, - label, - description, - status = "complete", - children, - ...props - }: ChainOfThoughtStepProps) => { - const statusStyles = { - complete: "text-muted-foreground", - active: "text-foreground", - pending: "text-muted-foreground/50", - }; + ({ + className, + icon: Icon = DotIcon, + label, + description, + status = "complete", + children, + ...props + }: ChainOfThoughtStepProps) => { + const statusStyles = { + complete: "text-muted-foreground", + active: "text-foreground", + pending: "text-muted-foreground/50", + }; - return ( -
-
- -
-
-
-
{label}
- {description && ( -
{description}
- )} - {children} -
-
- ); - } + return ( +
+
+ +
+
+
+
{label}
+ {description && ( +
{description}
+ )} + {children} +
+
+ ); + }, ); export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">; export const ChainOfThoughtSearchResults = memo( - ({ className, ...props }: ChainOfThoughtSearchResultsProps) => ( -
- ) + ({ className, ...props }: ChainOfThoughtSearchResultsProps) => ( +
+ ), ); export type ChainOfThoughtSearchResultProps = ComponentProps; export const ChainOfThoughtSearchResult = memo( - ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => ( - - {children} - - ) + ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => ( + + {children} + + ), ); export type ChainOfThoughtContentProps = ComponentProps< - typeof CollapsibleContent + typeof CollapsibleContent >; export const ChainOfThoughtContent = memo( - ({ className, children, ...props }: ChainOfThoughtContentProps) => { - const { isOpen } = useChainOfThought(); + ({ className, children, ...props }: ChainOfThoughtContentProps) => { + const { isOpen } = useChainOfThought(); - return ( - - - {children} - - - ); - } + return ( + + + {children} + + + ); + }, ); export type ChainOfThoughtImageProps = ComponentProps<"div"> & { - caption?: string; + caption?: string; }; export const ChainOfThoughtImage = memo( - ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => ( -
-
- {children} -
- {caption &&

{caption}

} -
- ) + ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => ( +
+
+ {children} +
+ {caption &&

{caption}

} +
+ ), ); ChainOfThought.displayName = "ChainOfThought"; diff --git a/vite/src/components/ai-elements/checkpoint.tsx b/vite/src/components/ai-elements/checkpoint.tsx index d9a5d326c..d04607451 100644 --- a/vite/src/components/ai-elements/checkpoint.tsx +++ b/vite/src/components/ai-elements/checkpoint.tsx @@ -1,68 +1,71 @@ "use client"; +import { BookmarkIcon, type LucideProps } from "lucide-react"; +import type { ComponentProps, HTMLAttributes } from "react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { - Tooltip, - TooltipContent, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { BookmarkIcon, type LucideProps } from "lucide-react"; -import type { ComponentProps, HTMLAttributes } from "react"; export type CheckpointProps = HTMLAttributes; export const Checkpoint = ({ - className, - children, - ...props + className, + children, + ...props }: CheckpointProps) => ( -
- {children} - -
+
+ {children} + +
); export type CheckpointIconProps = LucideProps; export const CheckpointIcon = ({ - className, - children, - ...props + className, + children, + ...props }: CheckpointIconProps) => - children ?? ( - - ); + children ?? ( + + ); export type CheckpointTriggerProps = ComponentProps & { - tooltip?: string; + tooltip?: string; }; export const CheckpointTrigger = ({ - children, - className, - variant = "ghost", - size = "sm", - tooltip, - ...props + children, + className, + variant = "ghost", + size = "sm", + tooltip, + ...props }: CheckpointTriggerProps) => - tooltip ? ( - - - - - - {tooltip} - - - ) : ( - - ); + tooltip ? ( + + + + + + {tooltip} + + + ) : ( + + ); diff --git a/vite/src/components/ai-elements/code-block.tsx b/vite/src/components/ai-elements/code-block.tsx index b6865f0dc..50200fcf7 100644 --- a/vite/src/components/ai-elements/code-block.tsx +++ b/vite/src/components/ai-elements/code-block.tsx @@ -1,178 +1,178 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; import { CheckIcon, CopyIcon } from "lucide-react"; import { - type ComponentProps, - createContext, - type HTMLAttributes, - useContext, - useEffect, - useRef, - useState, + type ComponentProps, + createContext, + type HTMLAttributes, + useContext, + useEffect, + useRef, + useState, } from "react"; import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; type CodeBlockProps = HTMLAttributes & { - code: string; - language: BundledLanguage; - showLineNumbers?: boolean; + code: string; + language: BundledLanguage; + showLineNumbers?: boolean; }; type CodeBlockContextType = { - code: string; + code: string; }; const CodeBlockContext = createContext({ - code: "", + code: "", }); const lineNumberTransformer: ShikiTransformer = { - name: "line-numbers", - line(node, line) { - node.children.unshift({ - type: "element", - tagName: "span", - properties: { - className: [ - "inline-block", - "min-w-10", - "mr-4", - "text-right", - "select-none", - "text-muted-foreground", - ], - }, - children: [{ type: "text", value: String(line) }], - }); - }, + name: "line-numbers", + line(node, line) { + node.children.unshift({ + type: "element", + tagName: "span", + properties: { + className: [ + "inline-block", + "min-w-10", + "mr-4", + "text-right", + "select-none", + "text-muted-foreground", + ], + }, + children: [{ type: "text", value: String(line) }], + }); + }, }; export async function highlightCode( - code: string, - language: BundledLanguage, - showLineNumbers = false + code: string, + language: BundledLanguage, + showLineNumbers = false, ) { - const transformers: ShikiTransformer[] = showLineNumbers - ? [lineNumberTransformer] - : []; + const transformers: ShikiTransformer[] = showLineNumbers + ? [lineNumberTransformer] + : []; - return await Promise.all([ - codeToHtml(code, { - lang: language, - theme: "one-light", - transformers, - }), - codeToHtml(code, { - lang: language, - theme: "one-dark-pro", - transformers, - }), - ]); + return await Promise.all([ + codeToHtml(code, { + lang: language, + theme: "one-light", + transformers, + }), + codeToHtml(code, { + lang: language, + theme: "one-dark-pro", + transformers, + }), + ]); } export const CodeBlock = ({ - code, - language, - showLineNumbers = false, - className, - children, - ...props + code, + language, + showLineNumbers = false, + className, + children, + ...props }: CodeBlockProps) => { - const [html, setHtml] = useState(""); - const [darkHtml, setDarkHtml] = useState(""); - const mounted = useRef(false); + const [html, setHtml] = useState(""); + const [darkHtml, setDarkHtml] = useState(""); + const mounted = useRef(false); - useEffect(() => { - highlightCode(code, language, showLineNumbers).then(([light, dark]) => { - if (!mounted.current) { - setHtml(light); - setDarkHtml(dark); - mounted.current = true; - } - }); + useEffect(() => { + highlightCode(code, language, showLineNumbers).then(([light, dark]) => { + if (!mounted.current) { + setHtml(light); + setDarkHtml(dark); + mounted.current = true; + } + }); - return () => { - mounted.current = false; - }; - }, [code, language, showLineNumbers]); + return () => { + mounted.current = false; + }; + }, [code, language, showLineNumbers]); - return ( - -
-
-
-
- {children && ( -
- {children} -
- )} -
-
- - ); + return ( + +
+
+
+
+ {children && ( +
+ {children} +
+ )} +
+
+ + ); }; export type CodeBlockCopyButtonProps = ComponentProps & { - onCopy?: () => void; - onError?: (error: Error) => void; - timeout?: number; + onCopy?: () => void; + onError?: (error: Error) => void; + timeout?: number; }; export const CodeBlockCopyButton = ({ - onCopy, - onError, - timeout = 2000, - children, - className, - ...props + onCopy, + onError, + timeout = 2000, + children, + className, + ...props }: CodeBlockCopyButtonProps) => { - const [isCopied, setIsCopied] = useState(false); - const { code } = useContext(CodeBlockContext); + const [isCopied, setIsCopied] = useState(false); + const { code } = useContext(CodeBlockContext); - const copyToClipboard = async () => { - if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { - onError?.(new Error("Clipboard API not available")); - return; - } + const copyToClipboard = async () => { + if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { + onError?.(new Error("Clipboard API not available")); + return; + } - try { - await navigator.clipboard.writeText(code); - setIsCopied(true); - onCopy?.(); - setTimeout(() => setIsCopied(false), timeout); - } catch (error) { - onError?.(error as Error); - } - }; + try { + await navigator.clipboard.writeText(code); + setIsCopied(true); + onCopy?.(); + setTimeout(() => setIsCopied(false), timeout); + } catch (error) { + onError?.(error as Error); + } + }; - const Icon = isCopied ? CheckIcon : CopyIcon; + const Icon = isCopied ? CheckIcon : CopyIcon; - return ( - - ); + return ( + + ); }; diff --git a/vite/src/components/ai-elements/confirmation.tsx b/vite/src/components/ai-elements/confirmation.tsx index 1cd9b7ff8..b4fe0c963 100644 --- a/vite/src/components/ai-elements/confirmation.tsx +++ b/vite/src/components/ai-elements/confirmation.tsx @@ -1,182 +1,182 @@ "use client"; +import type { ToolUIPart } from "ai"; +import { + type ComponentProps, + createContext, + type ReactNode, + useContext, +} from "react"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import type { ToolUIPart } from "ai"; -import { - type ComponentProps, - createContext, - type ReactNode, - useContext, -} from "react"; type ToolUIPartApproval = - | { - id: string; - approved?: never; - reason?: never; - } - | { - id: string; - approved: boolean; - reason?: string; - } - | { - id: string; - approved: true; - reason?: string; - } - | { - id: string; - approved: true; - reason?: string; - } - | { - id: string; - approved: false; - reason?: string; - } - | undefined; + | { + id: string; + approved?: never; + reason?: never; + } + | { + id: string; + approved: boolean; + reason?: string; + } + | { + id: string; + approved: true; + reason?: string; + } + | { + id: string; + approved: true; + reason?: string; + } + | { + id: string; + approved: false; + reason?: string; + } + | undefined; type ConfirmationContextValue = { - approval: ToolUIPartApproval; - state: ToolUIPart["state"]; + approval: ToolUIPartApproval; + state: ToolUIPart["state"]; }; const ConfirmationContext = createContext( - null + null, ); const useConfirmation = () => { - const context = useContext(ConfirmationContext); + const context = useContext(ConfirmationContext); - if (!context) { - throw new Error("Confirmation components must be used within Confirmation"); - } + if (!context) { + throw new Error("Confirmation components must be used within Confirmation"); + } - return context; + return context; }; export type ConfirmationProps = ComponentProps & { - approval?: ToolUIPartApproval; - state: ToolUIPart["state"]; + approval?: ToolUIPartApproval; + state: ToolUIPart["state"]; }; export const Confirmation = ({ - className, - approval, - state, - ...props + className, + approval, + state, + ...props }: ConfirmationProps) => { - if (!approval || state === "input-streaming" || state === "input-available") { - return null; - } + if (!approval || state === "input-streaming" || state === "input-available") { + return null; + } - return ( - - - - ); + return ( + + + + ); }; export type ConfirmationTitleProps = ComponentProps; export const ConfirmationTitle = ({ - className, - ...props + className, + ...props }: ConfirmationTitleProps) => ( - + ); export type ConfirmationRequestProps = { - children?: ReactNode; + children?: ReactNode; }; export const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => { - const { state } = useConfirmation(); + const { state } = useConfirmation(); - // Only show when approval is requested - // @ts-expect-error state only available in AI SDK v6 - if (state !== "approval-requested") { - return null; - } + // Only show when approval is requested + // @ts-expect-error state only available in AI SDK v6 + if (state !== "approval-requested") { + return null; + } - return children; + return children; }; export type ConfirmationAcceptedProps = { - children?: ReactNode; + children?: ReactNode; }; export const ConfirmationAccepted = ({ - children, + children, }: ConfirmationAcceptedProps) => { - const { approval, state } = useConfirmation(); + const { approval, state } = useConfirmation(); - // Only show when approved and in response states - if ( - !approval?.approved || - // @ts-expect-error state only available in AI SDK v6 - (state !== "approval-responded" && - // @ts-expect-error state only available in AI SDK v6 - state !== "output-denied" && - state !== "output-available") - ) { - return null; - } + // Only show when approved and in response states + if ( + !approval?.approved || + // @ts-expect-error state only available in AI SDK v6 + (state !== "approval-responded" && + // @ts-expect-error state only available in AI SDK v6 + state !== "output-denied" && + state !== "output-available") + ) { + return null; + } - return children; + return children; }; export type ConfirmationRejectedProps = { - children?: ReactNode; + children?: ReactNode; }; export const ConfirmationRejected = ({ - children, + children, }: ConfirmationRejectedProps) => { - const { approval, state } = useConfirmation(); + const { approval, state } = useConfirmation(); - // Only show when rejected and in response states - if ( - approval?.approved !== false || - // @ts-expect-error state only available in AI SDK v6 - (state !== "approval-responded" && - // @ts-expect-error state only available in AI SDK v6 - state !== "output-denied" && - state !== "output-available") - ) { - return null; - } + // Only show when rejected and in response states + if ( + approval?.approved !== false || + // @ts-expect-error state only available in AI SDK v6 + (state !== "approval-responded" && + // @ts-expect-error state only available in AI SDK v6 + state !== "output-denied" && + state !== "output-available") + ) { + return null; + } - return children; + return children; }; export type ConfirmationActionsProps = ComponentProps<"div">; export const ConfirmationActions = ({ - className, - ...props + className, + ...props }: ConfirmationActionsProps) => { - const { state } = useConfirmation(); + const { state } = useConfirmation(); - // Only show when approval is requested - // @ts-expect-error state only available in AI SDK v6 - if (state !== "approval-requested") { - return null; - } + // Only show when approval is requested + // @ts-expect-error state only available in AI SDK v6 + if (state !== "approval-requested") { + return null; + } - return ( -
- ); + return ( +
+ ); }; export type ConfirmationActionProps = ComponentProps; export const ConfirmationAction = (props: ConfirmationActionProps) => ( - - )} - - ); + return ( + + {children ?? ( + + )} + + ); }; export type ContextContentProps = ComponentProps; export const ContextContent = ({ - className, - ...props + className, + ...props }: ContextContentProps) => ( - + ); export type ContextContentHeaderProps = ComponentProps<"div">; export const ContextContentHeader = ({ - children, - className, - ...props + children, + className, + ...props }: ContextContentHeaderProps) => { - const { usedTokens, maxTokens } = useContextValue(); - const usedPercent = usedTokens / maxTokens; - const displayPct = new Intl.NumberFormat("en-US", { - style: "percent", - maximumFractionDigits: 1, - }).format(usedPercent); - const used = new Intl.NumberFormat("en-US", { - notation: "compact", - }).format(usedTokens); - const total = new Intl.NumberFormat("en-US", { - notation: "compact", - }).format(maxTokens); + const { usedTokens, maxTokens } = useContextValue(); + const usedPercent = usedTokens / maxTokens; + const displayPct = new Intl.NumberFormat("en-US", { + style: "percent", + maximumFractionDigits: 1, + }).format(usedPercent); + const used = new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(usedTokens); + const total = new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(maxTokens); - return ( -
- {children ?? ( - <> -
-

{displayPct}

-

- {used} / {total} -

-
-
- -
- - )} -
- ); + return ( +
+ {children ?? ( + <> +
+

{displayPct}

+

+ {used} / {total} +

+
+
+ +
+ + )} +
+ ); }; export type ContextContentBodyProps = ComponentProps<"div">; export const ContextContentBody = ({ - children, - className, - ...props + children, + className, + ...props }: ContextContentBodyProps) => ( -
- {children} -
+
+ {children} +
); export type ContextContentFooterProps = ComponentProps<"div">; export const ContextContentFooter = ({ - children, - className, - ...props + children, + className, + ...props }: ContextContentFooterProps) => { - const { modelId, usage } = useContextValue(); - const costUSD = modelId - ? getUsage({ - modelId, - usage: { - input: usage?.inputTokens ?? 0, - output: usage?.outputTokens ?? 0, - }, - }).costUSD?.totalUSD - : undefined; - const totalCost = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(costUSD ?? 0); + const { modelId, usage } = useContextValue(); + const costUSD = modelId + ? getUsage({ + modelId, + usage: { + input: usage?.inputTokens ?? 0, + output: usage?.outputTokens ?? 0, + }, + }).costUSD?.totalUSD + : undefined; + const totalCost = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(costUSD ?? 0); - return ( -
- {children ?? ( - <> - Total cost - {totalCost} - - )} -
- ); + return ( +
+ {children ?? ( + <> + Total cost + {totalCost} + + )} +
+ ); }; export type ContextInputUsageProps = ComponentProps<"div">; export const ContextInputUsage = ({ - className, - children, - ...props + className, + children, + ...props }: ContextInputUsageProps) => { - const { usage, modelId } = useContextValue(); - const inputTokens = usage?.inputTokens ?? 0; + const { usage, modelId } = useContextValue(); + const inputTokens = usage?.inputTokens ?? 0; - if (children) { - return children; - } + if (children) { + return children; + } - if (!inputTokens) { - return null; - } + if (!inputTokens) { + return null; + } - const inputCost = modelId - ? getUsage({ - modelId, - usage: { input: inputTokens, output: 0 }, - }).costUSD?.totalUSD - : undefined; - const inputCostText = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(inputCost ?? 0); + const inputCost = modelId + ? getUsage({ + modelId, + usage: { input: inputTokens, output: 0 }, + }).costUSD?.totalUSD + : undefined; + const inputCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(inputCost ?? 0); - return ( -
- Input - -
- ); + return ( +
+ Input + +
+ ); }; export type ContextOutputUsageProps = ComponentProps<"div">; export const ContextOutputUsage = ({ - className, - children, - ...props + className, + children, + ...props }: ContextOutputUsageProps) => { - const { usage, modelId } = useContextValue(); - const outputTokens = usage?.outputTokens ?? 0; + const { usage, modelId } = useContextValue(); + const outputTokens = usage?.outputTokens ?? 0; - if (children) { - return children; - } + if (children) { + return children; + } - if (!outputTokens) { - return null; - } + if (!outputTokens) { + return null; + } - const outputCost = modelId - ? getUsage({ - modelId, - usage: { input: 0, output: outputTokens }, - }).costUSD?.totalUSD - : undefined; - const outputCostText = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(outputCost ?? 0); + const outputCost = modelId + ? getUsage({ + modelId, + usage: { input: 0, output: outputTokens }, + }).costUSD?.totalUSD + : undefined; + const outputCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(outputCost ?? 0); - return ( -
- Output - -
- ); + return ( +
+ Output + +
+ ); }; export type ContextReasoningUsageProps = ComponentProps<"div">; export const ContextReasoningUsage = ({ - className, - children, - ...props + className, + children, + ...props }: ContextReasoningUsageProps) => { - const { usage, modelId } = useContextValue(); - const reasoningTokens = usage?.reasoningTokens ?? 0; + const { usage, modelId } = useContextValue(); + const reasoningTokens = usage?.reasoningTokens ?? 0; - if (children) { - return children; - } + if (children) { + return children; + } - if (!reasoningTokens) { - return null; - } + if (!reasoningTokens) { + return null; + } - const reasoningCost = modelId - ? getUsage({ - modelId, - usage: { reasoningTokens }, - }).costUSD?.totalUSD - : undefined; - const reasoningCostText = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(reasoningCost ?? 0); + const reasoningCost = modelId + ? getUsage({ + modelId, + usage: { reasoningTokens }, + }).costUSD?.totalUSD + : undefined; + const reasoningCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(reasoningCost ?? 0); - return ( -
- Reasoning - -
- ); + return ( +
+ Reasoning + +
+ ); }; export type ContextCacheUsageProps = ComponentProps<"div">; export const ContextCacheUsage = ({ - className, - children, - ...props + className, + children, + ...props }: ContextCacheUsageProps) => { - const { usage, modelId } = useContextValue(); - const cacheTokens = usage?.cachedInputTokens ?? 0; + const { usage, modelId } = useContextValue(); + const cacheTokens = usage?.cachedInputTokens ?? 0; - if (children) { - return children; - } + if (children) { + return children; + } - if (!cacheTokens) { - return null; - } + if (!cacheTokens) { + return null; + } - const cacheCost = modelId - ? getUsage({ - modelId, - usage: { cacheReads: cacheTokens, input: 0, output: 0 }, - }).costUSD?.totalUSD - : undefined; - const cacheCostText = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(cacheCost ?? 0); + const cacheCost = modelId + ? getUsage({ + modelId, + usage: { cacheReads: cacheTokens, input: 0, output: 0 }, + }).costUSD?.totalUSD + : undefined; + const cacheCostText = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(cacheCost ?? 0); - return ( -
- Cache - -
- ); + return ( +
+ Cache + +
+ ); }; const TokensWithCost = ({ - tokens, - costText, + tokens, + costText, }: { - tokens?: number; - costText?: string; + tokens?: number; + costText?: string; }) => ( - - {tokens === undefined - ? "—" - : new Intl.NumberFormat("en-US", { - notation: "compact", - }).format(tokens)} - {costText ? ( - • {costText} - ) : null} - + + {tokens === undefined + ? "—" + : new Intl.NumberFormat("en-US", { + notation: "compact", + }).format(tokens)} + {costText ? ( + • {costText} + ) : null} + ); diff --git a/vite/src/components/ai-elements/controls.tsx b/vite/src/components/ai-elements/controls.tsx index 770a8262a..fdd119b0a 100644 --- a/vite/src/components/ai-elements/controls.tsx +++ b/vite/src/components/ai-elements/controls.tsx @@ -1,18 +1,18 @@ "use client"; -import { cn } from "@/lib/utils"; import { Controls as ControlsPrimitive } from "@xyflow/react"; import type { ComponentProps } from "react"; +import { cn } from "@/lib/utils"; export type ControlsProps = ComponentProps; export const Controls = ({ className, ...props }: ControlsProps) => ( - button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!", - className - )} - {...props} - /> + button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!", + className, + )} + {...props} + /> ); diff --git a/vite/src/components/ai-elements/conversation.tsx b/vite/src/components/ai-elements/conversation.tsx index aa380f573..1ec05f256 100644 --- a/vite/src/components/ai-elements/conversation.tsx +++ b/vite/src/components/ai-elements/conversation.tsx @@ -1,100 +1,100 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; import { ArrowDownIcon } from "lucide-react"; import type { ComponentProps } from "react"; import { useCallback } from "react"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; export type ConversationProps = ComponentProps; export const Conversation = ({ className, ...props }: ConversationProps) => ( - + ); export type ConversationContentProps = ComponentProps< - typeof StickToBottom.Content + typeof StickToBottom.Content >; export const ConversationContent = ({ - className, - ...props + className, + ...props }: ConversationContentProps) => ( - + ); export type ConversationEmptyStateProps = ComponentProps<"div"> & { - title?: string; - description?: string; - icon?: React.ReactNode; + title?: string; + description?: string; + icon?: React.ReactNode; }; export const ConversationEmptyState = ({ - className, - title = "No messages yet", - description = "Start a conversation to see messages here", - icon, - children, - ...props + className, + title = "No messages yet", + description = "Start a conversation to see messages here", + icon, + children, + ...props }: ConversationEmptyStateProps) => ( -
- {children ?? ( - <> - {icon &&
{icon}
} -
-

{title}

- {description && ( -

{description}

- )} -
- - )} -
+
+ {children ?? ( + <> + {icon &&
{icon}
} +
+

{title}

+ {description && ( +

{description}

+ )} +
+ + )} +
); export type ConversationScrollButtonProps = ComponentProps; export const ConversationScrollButton = ({ - className, - ...props + className, + ...props }: ConversationScrollButtonProps) => { - const { isAtBottom, scrollToBottom } = useStickToBottomContext(); + const { isAtBottom, scrollToBottom } = useStickToBottomContext(); - const handleScrollToBottom = useCallback(() => { - scrollToBottom(); - }, [scrollToBottom]); + const handleScrollToBottom = useCallback(() => { + scrollToBottom(); + }, [scrollToBottom]); - return ( - !isAtBottom && ( - - ) - ); + return ( + !isAtBottom && ( + + ) + ); }; diff --git a/vite/src/components/ai-elements/edge.tsx b/vite/src/components/ai-elements/edge.tsx index 3cec409d1..3bd9a8997 100644 --- a/vite/src/components/ai-elements/edge.tsx +++ b/vite/src/components/ai-elements/edge.tsx @@ -1,140 +1,140 @@ import { - BaseEdge, - type EdgeProps, - getBezierPath, - getSimpleBezierPath, - type InternalNode, - type Node, - Position, - useInternalNode, + BaseEdge, + type EdgeProps, + getBezierPath, + getSimpleBezierPath, + type InternalNode, + type Node, + Position, + useInternalNode, } from "@xyflow/react"; const Temporary = ({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, }: EdgeProps) => { - const [edgePath] = getSimpleBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); + const [edgePath] = getSimpleBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); - return ( - - ); + return ( + + ); }; const getHandleCoordsByPosition = ( - node: InternalNode, - handlePosition: Position + node: InternalNode, + handlePosition: Position, ) => { - // Choose the handle type based on position - Left is for target, Right is for source - const handleType = handlePosition === Position.Left ? "target" : "source"; + // Choose the handle type based on position - Left is for target, Right is for source + const handleType = handlePosition === Position.Left ? "target" : "source"; - const handle = node.internals.handleBounds?.[handleType]?.find( - (h) => h.position === handlePosition - ); + const handle = node.internals.handleBounds?.[handleType]?.find( + (h) => h.position === handlePosition, + ); - if (!handle) { - return [0, 0] as const; - } + if (!handle) { + return [0, 0] as const; + } - let offsetX = handle.width / 2; - let offsetY = handle.height / 2; + let offsetX = handle.width / 2; + let offsetY = handle.height / 2; - // this is a tiny detail to make the markerEnd of an edge visible. - // The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset - // when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position - switch (handlePosition) { - case Position.Left: - offsetX = 0; - break; - case Position.Right: - offsetX = handle.width; - break; - case Position.Top: - offsetY = 0; - break; - case Position.Bottom: - offsetY = handle.height; - break; - default: - throw new Error(`Invalid handle position: ${handlePosition}`); - } + // this is a tiny detail to make the markerEnd of an edge visible. + // The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset + // when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position + switch (handlePosition) { + case Position.Left: + offsetX = 0; + break; + case Position.Right: + offsetX = handle.width; + break; + case Position.Top: + offsetY = 0; + break; + case Position.Bottom: + offsetY = handle.height; + break; + default: + throw new Error(`Invalid handle position: ${handlePosition}`); + } - const x = node.internals.positionAbsolute.x + handle.x + offsetX; - const y = node.internals.positionAbsolute.y + handle.y + offsetY; + const x = node.internals.positionAbsolute.x + handle.x + offsetX; + const y = node.internals.positionAbsolute.y + handle.y + offsetY; - return [x, y] as const; + return [x, y] as const; }; const getEdgeParams = ( - source: InternalNode, - target: InternalNode + source: InternalNode, + target: InternalNode, ) => { - const sourcePos = Position.Right; - const [sx, sy] = getHandleCoordsByPosition(source, sourcePos); - const targetPos = Position.Left; - const [tx, ty] = getHandleCoordsByPosition(target, targetPos); + const sourcePos = Position.Right; + const [sx, sy] = getHandleCoordsByPosition(source, sourcePos); + const targetPos = Position.Left; + const [tx, ty] = getHandleCoordsByPosition(target, targetPos); - return { - sx, - sy, - tx, - ty, - sourcePos, - targetPos, - }; + return { + sx, + sy, + tx, + ty, + sourcePos, + targetPos, + }; }; const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => { - const sourceNode = useInternalNode(source); - const targetNode = useInternalNode(target); + const sourceNode = useInternalNode(source); + const targetNode = useInternalNode(target); - if (!(sourceNode && targetNode)) { - return null; - } + if (!(sourceNode && targetNode)) { + return null; + } - const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams( - sourceNode, - targetNode - ); + const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams( + sourceNode, + targetNode, + ); - const [edgePath] = getBezierPath({ - sourceX: sx, - sourceY: sy, - sourcePosition: sourcePos, - targetX: tx, - targetY: ty, - targetPosition: targetPos, - }); + const [edgePath] = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: sourcePos, + targetX: tx, + targetY: ty, + targetPosition: targetPos, + }); - return ( - <> - - - - - - ); + return ( + <> + + + + + + ); }; export const Edge = { - Temporary, - Animated, + Temporary, + Animated, }; diff --git a/vite/src/components/ai-elements/image.tsx b/vite/src/components/ai-elements/image.tsx index 542812a32..5fac5fca3 100644 --- a/vite/src/components/ai-elements/image.tsx +++ b/vite/src/components/ai-elements/image.tsx @@ -1,24 +1,24 @@ -import { cn } from "@/lib/utils"; import type { Experimental_GeneratedImage } from "ai"; +import { cn } from "@/lib/utils"; export type ImageProps = Experimental_GeneratedImage & { - className?: string; - alt?: string; + className?: string; + alt?: string; }; export const Image = ({ - base64, - uint8Array, - mediaType, - ...props + base64, + uint8Array, + mediaType, + ...props }: ImageProps) => ( - {props.alt} + {props.alt} ); diff --git a/vite/src/components/ai-elements/inline-citation.tsx b/vite/src/components/ai-elements/inline-citation.tsx index 5977081bb..8ea52296e 100644 --- a/vite/src/components/ai-elements/inline-citation.tsx +++ b/vite/src/components/ai-elements/inline-citation.tsx @@ -1,287 +1,287 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { - Carousel, - type CarouselApi, - CarouselContent, - CarouselItem, -} from "@/components/ui/carousel"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card"; -import { cn } from "@/lib/utils"; import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; import { - type ComponentProps, - createContext, - useCallback, - useContext, - useEffect, - useState, + type ComponentProps, + createContext, + useCallback, + useContext, + useEffect, + useState, } from "react"; +import { Badge } from "@/components/ui/badge"; +import { + Carousel, + type CarouselApi, + CarouselContent, + CarouselItem, +} from "@/components/ui/carousel"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { cn } from "@/lib/utils"; export type InlineCitationProps = ComponentProps<"span">; export const InlineCitation = ({ - className, - ...props + className, + ...props }: InlineCitationProps) => ( - + ); export type InlineCitationTextProps = ComponentProps<"span">; export const InlineCitationText = ({ - className, - ...props + className, + ...props }: InlineCitationTextProps) => ( - + ); export type InlineCitationCardProps = ComponentProps; export const InlineCitationCard = (props: InlineCitationCardProps) => ( - + ); export type InlineCitationCardTriggerProps = ComponentProps & { - sources: string[]; + sources: string[]; }; export const InlineCitationCardTrigger = ({ - sources, - className, - ...props + sources, + className, + ...props }: InlineCitationCardTriggerProps) => ( - - - {sources[0] ? ( - <> - {new URL(sources[0]).hostname}{" "} - {sources.length > 1 && `+${sources.length - 1}`} - - ) : ( - "unknown" - )} - - + + + {sources[0] ? ( + <> + {new URL(sources[0]).hostname}{" "} + {sources.length > 1 && `+${sources.length - 1}`} + + ) : ( + "unknown" + )} + + ); export type InlineCitationCardBodyProps = ComponentProps<"div">; export const InlineCitationCardBody = ({ - className, - ...props + className, + ...props }: InlineCitationCardBodyProps) => ( - + ); const CarouselApiContext = createContext(undefined); const useCarouselApi = () => { - const context = useContext(CarouselApiContext); - return context; + const context = useContext(CarouselApiContext); + return context; }; export type InlineCitationCarouselProps = ComponentProps; export const InlineCitationCarousel = ({ - className, - children, - ...props + className, + children, + ...props }: InlineCitationCarouselProps) => { - const [api, setApi] = useState(); + const [api, setApi] = useState(); - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); }; export type InlineCitationCarouselContentProps = ComponentProps<"div">; export const InlineCitationCarouselContent = ( - props: InlineCitationCarouselContentProps + props: InlineCitationCarouselContentProps, ) => ; export type InlineCitationCarouselItemProps = ComponentProps<"div">; export const InlineCitationCarouselItem = ({ - className, - ...props + className, + ...props }: InlineCitationCarouselItemProps) => ( - + ); export type InlineCitationCarouselHeaderProps = ComponentProps<"div">; export const InlineCitationCarouselHeader = ({ - className, - ...props + className, + ...props }: InlineCitationCarouselHeaderProps) => ( -
+
); export type InlineCitationCarouselIndexProps = ComponentProps<"div">; export const InlineCitationCarouselIndex = ({ - children, - className, - ...props + children, + className, + ...props }: InlineCitationCarouselIndexProps) => { - const api = useCarouselApi(); - const [current, setCurrent] = useState(0); - const [count, setCount] = useState(0); + const api = useCarouselApi(); + const [current, setCurrent] = useState(0); + const [count, setCount] = useState(0); - useEffect(() => { - if (!api) { - return; - } + useEffect(() => { + if (!api) { + return; + } - setCount(api.scrollSnapList().length); - setCurrent(api.selectedScrollSnap() + 1); + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); - api.on("select", () => { - setCurrent(api.selectedScrollSnap() + 1); - }); - }, [api]); + api.on("select", () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); - return ( -
- {children ?? `${current}/${count}`} -
- ); + return ( +
+ {children ?? `${current}/${count}`} +
+ ); }; export type InlineCitationCarouselPrevProps = ComponentProps<"button">; export const InlineCitationCarouselPrev = ({ - className, - ...props + className, + ...props }: InlineCitationCarouselPrevProps) => { - const api = useCarouselApi(); + const api = useCarouselApi(); - const handleClick = useCallback(() => { - if (api) { - api.scrollPrev(); - } - }, [api]); + const handleClick = useCallback(() => { + if (api) { + api.scrollPrev(); + } + }, [api]); - return ( - - ); + return ( + + ); }; export type InlineCitationCarouselNextProps = ComponentProps<"button">; export const InlineCitationCarouselNext = ({ - className, - ...props + className, + ...props }: InlineCitationCarouselNextProps) => { - const api = useCarouselApi(); + const api = useCarouselApi(); - const handleClick = useCallback(() => { - if (api) { - api.scrollNext(); - } - }, [api]); + const handleClick = useCallback(() => { + if (api) { + api.scrollNext(); + } + }, [api]); - return ( - - ); + return ( + + ); }; export type InlineCitationSourceProps = ComponentProps<"div"> & { - title?: string; - url?: string; - description?: string; + title?: string; + url?: string; + description?: string; }; export const InlineCitationSource = ({ - title, - url, - description, - className, - children, - ...props + title, + url, + description, + className, + children, + ...props }: InlineCitationSourceProps) => ( -
- {title && ( -

{title}

- )} - {url && ( -

{url}

- )} - {description && ( -

- {description} -

- )} - {children} -
+
+ {title && ( +

{title}

+ )} + {url && ( +

{url}

+ )} + {description && ( +

+ {description} +

+ )} + {children} +
); export type InlineCitationQuoteProps = ComponentProps<"blockquote">; export const InlineCitationQuote = ({ - children, - className, - ...props + children, + className, + ...props }: InlineCitationQuoteProps) => ( -
- {children} -
+
+ {children} +
); diff --git a/vite/src/components/ai-elements/loader.tsx b/vite/src/components/ai-elements/loader.tsx index 5f0cfce40..28c62b270 100644 --- a/vite/src/components/ai-elements/loader.tsx +++ b/vite/src/components/ai-elements/loader.tsx @@ -1,96 +1,96 @@ -import { cn } from "@/lib/utils"; import type { HTMLAttributes } from "react"; +import { cn } from "@/lib/utils"; type LoaderIconProps = { - size?: number; + size?: number; }; const LoaderIcon = ({ size = 16 }: LoaderIconProps) => ( - - Loader - - - - - - - - - - - - - - - - - - + + Loader + + + + + + + + + + + + + + + + + + ); export type LoaderProps = HTMLAttributes & { - size?: number; + size?: number; }; export const Loader = ({ className, size = 16, ...props }: LoaderProps) => ( -
- -
+
+ +
); diff --git a/vite/src/components/ai-elements/message.tsx b/vite/src/components/ai-elements/message.tsx index 73d6997ef..657b2dddb 100644 --- a/vite/src/components/ai-elements/message.tsx +++ b/vite/src/components/ai-elements/message.tsx @@ -1,448 +1,445 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - ButtonGroup, - ButtonGroupText, -} from "@/components/ui/button-group"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; import type { FileUIPart, UIMessage } from "ai"; import { - ChevronLeftIcon, - ChevronRightIcon, - PaperclipIcon, - XIcon, + ChevronLeftIcon, + ChevronRightIcon, + PaperclipIcon, + XIcon, } from "lucide-react"; import type { ComponentProps, HTMLAttributes, ReactElement } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react"; import { Streamdown } from "streamdown"; +import { Button } from "@/components/ui/button"; +import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; export type MessageProps = HTMLAttributes & { - from: UIMessage["role"]; + from: UIMessage["role"]; }; export const Message = ({ className, from, ...props }: MessageProps) => ( -
+
); export type MessageContentProps = HTMLAttributes; export const MessageContent = ({ - children, - className, - ...props + children, + className, + ...props }: MessageContentProps) => ( -
- {children} -
+
+ {children} +
); export type MessageActionsProps = ComponentProps<"div">; export const MessageActions = ({ - className, - children, - ...props + className, + children, + ...props }: MessageActionsProps) => ( -
- {children} -
+
+ {children} +
); export type MessageActionProps = ComponentProps & { - tooltip?: string; - label?: string; + tooltip?: string; + label?: string; }; export const MessageAction = ({ - tooltip, - children, - label, - variant = "ghost", - size = "icon-sm", - ...props + tooltip, + children, + label, + variant = "ghost", + size = "icon-sm", + ...props }: MessageActionProps) => { - const button = ( - - ); + const button = ( + + ); - if (tooltip) { - return ( - - - {button} - -

{tooltip}

-
-
-
- ); - } + if (tooltip) { + return ( + + + {button} + +

{tooltip}

+
+
+
+ ); + } - return button; + return button; }; type MessageBranchContextType = { - currentBranch: number; - totalBranches: number; - goToPrevious: () => void; - goToNext: () => void; - branches: ReactElement[]; - setBranches: (branches: ReactElement[]) => void; + currentBranch: number; + totalBranches: number; + goToPrevious: () => void; + goToNext: () => void; + branches: ReactElement[]; + setBranches: (branches: ReactElement[]) => void; }; const MessageBranchContext = createContext( - null + null, ); const useMessageBranch = () => { - const context = useContext(MessageBranchContext); + const context = useContext(MessageBranchContext); - if (!context) { - throw new Error( - "MessageBranch components must be used within MessageBranch" - ); - } + if (!context) { + throw new Error( + "MessageBranch components must be used within MessageBranch", + ); + } - return context; + return context; }; export type MessageBranchProps = HTMLAttributes & { - defaultBranch?: number; - onBranchChange?: (branchIndex: number) => void; + defaultBranch?: number; + onBranchChange?: (branchIndex: number) => void; }; export const MessageBranch = ({ - defaultBranch = 0, - onBranchChange, - className, - ...props + defaultBranch = 0, + onBranchChange, + className, + ...props }: MessageBranchProps) => { - const [currentBranch, setCurrentBranch] = useState(defaultBranch); - const [branches, setBranches] = useState([]); + const [currentBranch, setCurrentBranch] = useState(defaultBranch); + const [branches, setBranches] = useState([]); - const handleBranchChange = (newBranch: number) => { - setCurrentBranch(newBranch); - onBranchChange?.(newBranch); - }; + const handleBranchChange = (newBranch: number) => { + setCurrentBranch(newBranch); + onBranchChange?.(newBranch); + }; - const goToPrevious = () => { - const newBranch = - currentBranch > 0 ? currentBranch - 1 : branches.length - 1; - handleBranchChange(newBranch); - }; + const goToPrevious = () => { + const newBranch = + currentBranch > 0 ? currentBranch - 1 : branches.length - 1; + handleBranchChange(newBranch); + }; - const goToNext = () => { - const newBranch = - currentBranch < branches.length - 1 ? currentBranch + 1 : 0; - handleBranchChange(newBranch); - }; + const goToNext = () => { + const newBranch = + currentBranch < branches.length - 1 ? currentBranch + 1 : 0; + handleBranchChange(newBranch); + }; - const contextValue: MessageBranchContextType = { - currentBranch, - totalBranches: branches.length, - goToPrevious, - goToNext, - branches, - setBranches, - }; + const contextValue: MessageBranchContextType = { + currentBranch, + totalBranches: branches.length, + goToPrevious, + goToNext, + branches, + setBranches, + }; - return ( - -
div]:pb-0", className)} - {...props} - /> - - ); + return ( + +
div]:pb-0", className)} + {...props} + /> + + ); }; export type MessageBranchContentProps = HTMLAttributes; export const MessageBranchContent = ({ - children, - ...props + children, + ...props }: MessageBranchContentProps) => { - const { currentBranch, setBranches, branches } = useMessageBranch(); - const childrenArray = Array.isArray(children) ? children : [children]; + const { currentBranch, setBranches, branches } = useMessageBranch(); + const childrenArray = Array.isArray(children) ? children : [children]; - // Use useEffect to update branches when they change - useEffect(() => { - if (branches.length !== childrenArray.length) { - setBranches(childrenArray); - } - }, [childrenArray, branches, setBranches]); + // Use useEffect to update branches when they change + useEffect(() => { + if (branches.length !== childrenArray.length) { + setBranches(childrenArray); + } + }, [childrenArray, branches, setBranches]); - return childrenArray.map((branch, index) => ( -
div]:pb-0", - index === currentBranch ? "block" : "hidden" - )} - key={branch.key} - {...props} - > - {branch} -
- )); + return childrenArray.map((branch, index) => ( +
div]:pb-0", + index === currentBranch ? "block" : "hidden", + )} + key={branch.key} + {...props} + > + {branch} +
+ )); }; export type MessageBranchSelectorProps = HTMLAttributes & { - from: UIMessage["role"]; + from: UIMessage["role"]; }; export const MessageBranchSelector = ({ - className, - from, - ...props + className, + from, + ...props }: MessageBranchSelectorProps) => { - const { totalBranches } = useMessageBranch(); + const { totalBranches } = useMessageBranch(); - // Don't render if there's only one branch - if (totalBranches <= 1) { - return null; - } + // Don't render if there's only one branch + if (totalBranches <= 1) { + return null; + } - return ( - - ); + return ( + + ); }; export type MessageBranchPreviousProps = ComponentProps; export const MessageBranchPrevious = ({ - children, - ...props + children, + ...props }: MessageBranchPreviousProps) => { - const { goToPrevious, totalBranches } = useMessageBranch(); + const { goToPrevious, totalBranches } = useMessageBranch(); - return ( - - ); + return ( + + ); }; export type MessageBranchNextProps = ComponentProps; export const MessageBranchNext = ({ - children, - className, - ...props + children, + className, + ...props }: MessageBranchNextProps) => { - const { goToNext, totalBranches } = useMessageBranch(); + const { goToNext, totalBranches } = useMessageBranch(); - return ( - - ); + return ( + + ); }; export type MessageBranchPageProps = HTMLAttributes; export const MessageBranchPage = ({ - className, - ...props + className, + ...props }: MessageBranchPageProps) => { - const { currentBranch, totalBranches } = useMessageBranch(); + const { currentBranch, totalBranches } = useMessageBranch(); - return ( - - {currentBranch + 1} of {totalBranches} - - ); + return ( + + {currentBranch + 1} of {totalBranches} + + ); }; export type MessageResponseProps = ComponentProps; export const MessageResponse = memo( - ({ className, ...props }: MessageResponseProps) => ( - *:first-child]:mt-0 [&>*:last-child]:mb-0", - className - )} - {...props} - /> - ), - (prevProps, nextProps) => prevProps.children === nextProps.children + ({ className, ...props }: MessageResponseProps) => ( + *:first-child]:mt-0 [&>*:last-child]:mb-0", + className, + )} + {...props} + /> + ), + (prevProps, nextProps) => prevProps.children === nextProps.children, ); MessageResponse.displayName = "MessageResponse"; export type MessageAttachmentProps = HTMLAttributes & { - data: FileUIPart; - className?: string; - onRemove?: () => void; + data: FileUIPart; + className?: string; + onRemove?: () => void; }; export function MessageAttachment({ - data, - className, - onRemove, - ...props + data, + className, + onRemove, + ...props }: MessageAttachmentProps) { - const filename = data.filename || ""; - const mediaType = - data.mediaType?.startsWith("image/") && data.url ? "image" : "file"; - const isImage = mediaType === "image"; - const attachmentLabel = filename || (isImage ? "Image" : "Attachment"); + const filename = data.filename || ""; + const mediaType = + data.mediaType?.startsWith("image/") && data.url ? "image" : "file"; + const isImage = mediaType === "image"; + const attachmentLabel = filename || (isImage ? "Image" : "Attachment"); - return ( -
- {isImage ? ( - <> - {filename - {onRemove && ( - - )} - - ) : ( - <> - - -
- -
-
- -

{attachmentLabel}

-
-
- {onRemove && ( - - )} - - )} -
- ); + return ( +
+ {isImage ? ( + <> + {filename + {onRemove && ( + + )} + + ) : ( + <> + + +
+ +
+
+ +

{attachmentLabel}

+
+
+ {onRemove && ( + + )} + + )} +
+ ); } export type MessageAttachmentsProps = ComponentProps<"div">; export function MessageAttachments({ - children, - className, - ...props + children, + className, + ...props }: MessageAttachmentsProps) { - if (!children) { - return null; - } + if (!children) { + return null; + } - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); } export type MessageToolbarProps = ComponentProps<"div">; export const MessageToolbar = ({ - className, - children, - ...props + className, + children, + ...props }: MessageToolbarProps) => ( -
- {children} -
+
+ {children} +
); diff --git a/vite/src/components/ai-elements/model-selector.tsx b/vite/src/components/ai-elements/model-selector.tsx index ef6ebd7e8..35080ddd1 100644 --- a/vite/src/components/ai-elements/model-selector.tsx +++ b/vite/src/components/ai-elements/model-selector.tsx @@ -1,205 +1,205 @@ +import type { ComponentProps, ReactNode } from "react"; import { - Command, - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, - CommandShortcut, + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, } from "@/components/ui/command"; import { - Dialog, - DialogContent, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; -import type { ComponentProps, ReactNode } from "react"; export type ModelSelectorProps = ComponentProps; export const ModelSelector = (props: ModelSelectorProps) => ( - + ); export type ModelSelectorTriggerProps = ComponentProps; export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => ( - + ); export type ModelSelectorContentProps = ComponentProps & { - title?: ReactNode; + title?: ReactNode; }; export const ModelSelectorContent = ({ - className, - children, - title = "Model Selector", - ...props + className, + children, + title = "Model Selector", + ...props }: ModelSelectorContentProps) => ( - - {title} - - {children} - - + + {title} + + {children} + + ); export type ModelSelectorDialogProps = ComponentProps; export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => ( - + ); export type ModelSelectorInputProps = ComponentProps; export const ModelSelectorInput = ({ - className, - ...props + className, + ...props }: ModelSelectorInputProps) => ( - + ); export type ModelSelectorListProps = ComponentProps; export const ModelSelectorList = (props: ModelSelectorListProps) => ( - + ); export type ModelSelectorEmptyProps = ComponentProps; export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => ( - + ); export type ModelSelectorGroupProps = ComponentProps; export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => ( - + ); export type ModelSelectorItemProps = ComponentProps; export const ModelSelectorItem = (props: ModelSelectorItemProps) => ( - + ); export type ModelSelectorShortcutProps = ComponentProps; export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => ( - + ); export type ModelSelectorSeparatorProps = ComponentProps< - typeof CommandSeparator + typeof CommandSeparator >; export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => ( - + ); export type ModelSelectorLogoProps = Omit< - ComponentProps<"img">, - "src" | "alt" + ComponentProps<"img">, + "src" | "alt" > & { - provider: - | "moonshotai-cn" - | "lucidquery" - | "moonshotai" - | "zai-coding-plan" - | "alibaba" - | "xai" - | "vultr" - | "nvidia" - | "upstage" - | "groq" - | "github-copilot" - | "mistral" - | "vercel" - | "nebius" - | "deepseek" - | "alibaba-cn" - | "google-vertex-anthropic" - | "venice" - | "chutes" - | "cortecs" - | "github-models" - | "togetherai" - | "azure" - | "baseten" - | "huggingface" - | "opencode" - | "fastrouter" - | "google" - | "google-vertex" - | "cloudflare-workers-ai" - | "inception" - | "wandb" - | "openai" - | "zhipuai-coding-plan" - | "perplexity" - | "openrouter" - | "zenmux" - | "v0" - | "iflowcn" - | "synthetic" - | "deepinfra" - | "zhipuai" - | "submodel" - | "zai" - | "inference" - | "requesty" - | "morph" - | "lmstudio" - | "anthropic" - | "aihubmix" - | "fireworks-ai" - | "modelscope" - | "llama" - | "scaleway" - | "amazon-bedrock" - | "cerebras" - | (string & {}); + provider: + | "moonshotai-cn" + | "lucidquery" + | "moonshotai" + | "zai-coding-plan" + | "alibaba" + | "xai" + | "vultr" + | "nvidia" + | "upstage" + | "groq" + | "github-copilot" + | "mistral" + | "vercel" + | "nebius" + | "deepseek" + | "alibaba-cn" + | "google-vertex-anthropic" + | "venice" + | "chutes" + | "cortecs" + | "github-models" + | "togetherai" + | "azure" + | "baseten" + | "huggingface" + | "opencode" + | "fastrouter" + | "google" + | "google-vertex" + | "cloudflare-workers-ai" + | "inception" + | "wandb" + | "openai" + | "zhipuai-coding-plan" + | "perplexity" + | "openrouter" + | "zenmux" + | "v0" + | "iflowcn" + | "synthetic" + | "deepinfra" + | "zhipuai" + | "submodel" + | "zai" + | "inference" + | "requesty" + | "morph" + | "lmstudio" + | "anthropic" + | "aihubmix" + | "fireworks-ai" + | "modelscope" + | "llama" + | "scaleway" + | "amazon-bedrock" + | "cerebras" + | (string & {}); }; export const ModelSelectorLogo = ({ - provider, - className, - ...props + provider, + className, + ...props }: ModelSelectorLogoProps) => ( - {`${provider} + {`${provider} ); export type ModelSelectorLogoGroupProps = ComponentProps<"div">; export const ModelSelectorLogoGroup = ({ - className, - ...props + className, + ...props }: ModelSelectorLogoGroupProps) => ( -
img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground", - className - )} - {...props} - /> +
img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground", + className, + )} + {...props} + /> ); export type ModelSelectorNameProps = ComponentProps<"span">; export const ModelSelectorName = ({ - className, - ...props + className, + ...props }: ModelSelectorNameProps) => ( - + ); diff --git a/vite/src/components/ai-elements/node.tsx b/vite/src/components/ai-elements/node.tsx index 75ac59a15..b58406985 100644 --- a/vite/src/components/ai-elements/node.tsx +++ b/vite/src/components/ai-elements/node.tsx @@ -1,44 +1,44 @@ -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { cn } from "@/lib/utils"; import { Handle, Position } from "@xyflow/react"; import type { ComponentProps } from "react"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { cn } from "@/lib/utils"; export type NodeProps = ComponentProps & { - handles: { - target: boolean; - source: boolean; - }; + handles: { + target: boolean; + source: boolean; + }; }; export const Node = ({ handles, className, ...props }: NodeProps) => ( - - {handles.target && } - {handles.source && } - {props.children} - + + {handles.target && } + {handles.source && } + {props.children} + ); export type NodeHeaderProps = ComponentProps; export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => ( - + ); export type NodeTitleProps = ComponentProps; @@ -48,7 +48,7 @@ export const NodeTitle = (props: NodeTitleProps) => ; export type NodeDescriptionProps = ComponentProps; export const NodeDescription = (props: NodeDescriptionProps) => ( - + ); export type NodeActionProps = ComponentProps; @@ -58,14 +58,14 @@ export const NodeAction = (props: NodeActionProps) => ; export type NodeContentProps = ComponentProps; export const NodeContent = ({ className, ...props }: NodeContentProps) => ( - + ); export type NodeFooterProps = ComponentProps; export const NodeFooter = ({ className, ...props }: NodeFooterProps) => ( - + ); diff --git a/vite/src/components/ai-elements/open-in-chat.tsx b/vite/src/components/ai-elements/open-in-chat.tsx index 0c62a6ac4..acc6b1d3d 100644 --- a/vite/src/components/ai-elements/open-in-chat.tsx +++ b/vite/src/components/ai-elements/open-in-chat.tsx @@ -1,365 +1,365 @@ "use client"; -import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { cn } from "@/lib/utils"; -import { - ChevronDownIcon, - ExternalLinkIcon, - MessageCircleIcon, + ChevronDownIcon, + ExternalLinkIcon, + MessageCircleIcon, } from "lucide-react"; import { type ComponentProps, createContext, useContext } from "react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; const providers = { - github: { - title: "Open in GitHub", - createUrl: (url: string) => url, - icon: ( - - GitHub - - - ), - }, - scira: { - title: "Open in Scira", - createUrl: (q: string) => - `https://scira.ai/?${new URLSearchParams({ - q, - })}`, - icon: ( - - Scira AI - - - - - - - - - ), - }, - chatgpt: { - title: "Open in ChatGPT", - createUrl: (prompt: string) => - `https://chatgpt.com/?${new URLSearchParams({ - hints: "search", - prompt, - })}`, - icon: ( - - OpenAI - - - ), - }, - claude: { - title: "Open in Claude", - createUrl: (q: string) => - `https://claude.ai/new?${new URLSearchParams({ - q, - })}`, - icon: ( - - Claude - - - ), - }, - t3: { - title: "Open in T3 Chat", - createUrl: (q: string) => - `https://t3.chat/new?${new URLSearchParams({ - q, - })}`, - icon: , - }, - v0: { - title: "Open in v0", - createUrl: (q: string) => - `https://v0.app?${new URLSearchParams({ - q, - })}`, - icon: ( - - v0 - - - - ), - }, - cursor: { - title: "Open in Cursor", - createUrl: (text: string) => { - const url = new URL("https://cursor.com/link/prompt"); - url.searchParams.set("text", text); - return url.toString(); - }, - icon: ( - - Cursor - - - ), - }, + github: { + title: "Open in GitHub", + createUrl: (url: string) => url, + icon: ( + + GitHub + + + ), + }, + scira: { + title: "Open in Scira", + createUrl: (q: string) => + `https://scira.ai/?${new URLSearchParams({ + q, + })}`, + icon: ( + + Scira AI + + + + + + + + + ), + }, + chatgpt: { + title: "Open in ChatGPT", + createUrl: (prompt: string) => + `https://chatgpt.com/?${new URLSearchParams({ + hints: "search", + prompt, + })}`, + icon: ( + + OpenAI + + + ), + }, + claude: { + title: "Open in Claude", + createUrl: (q: string) => + `https://claude.ai/new?${new URLSearchParams({ + q, + })}`, + icon: ( + + Claude + + + ), + }, + t3: { + title: "Open in T3 Chat", + createUrl: (q: string) => + `https://t3.chat/new?${new URLSearchParams({ + q, + })}`, + icon: , + }, + v0: { + title: "Open in v0", + createUrl: (q: string) => + `https://v0.app?${new URLSearchParams({ + q, + })}`, + icon: ( + + v0 + + + + ), + }, + cursor: { + title: "Open in Cursor", + createUrl: (text: string) => { + const url = new URL("https://cursor.com/link/prompt"); + url.searchParams.set("text", text); + return url.toString(); + }, + icon: ( + + Cursor + + + ), + }, }; const OpenInContext = createContext<{ query: string } | undefined>(undefined); const useOpenInContext = () => { - const context = useContext(OpenInContext); - if (!context) { - throw new Error("OpenIn components must be used within an OpenIn provider"); - } - return context; + const context = useContext(OpenInContext); + if (!context) { + throw new Error("OpenIn components must be used within an OpenIn provider"); + } + return context; }; export type OpenInProps = ComponentProps & { - query: string; + query: string; }; export const OpenIn = ({ query, ...props }: OpenInProps) => ( - - - + + + ); export type OpenInContentProps = ComponentProps; export const OpenInContent = ({ className, ...props }: OpenInContentProps) => ( - + ); export type OpenInItemProps = ComponentProps; export const OpenInItem = (props: OpenInItemProps) => ( - + ); export type OpenInLabelProps = ComponentProps; export const OpenInLabel = (props: OpenInLabelProps) => ( - + ); export type OpenInSeparatorProps = ComponentProps; export const OpenInSeparator = (props: OpenInSeparatorProps) => ( - + ); export type OpenInTriggerProps = ComponentProps; export const OpenInTrigger = ({ children, ...props }: OpenInTriggerProps) => ( - - {children ?? ( - - )} - + + {children ?? ( + + )} + ); export type OpenInChatGPTProps = ComponentProps; export const OpenInChatGPT = (props: OpenInChatGPTProps) => { - const { query } = useOpenInContext(); - return ( - - - {providers.chatgpt.icon} - {providers.chatgpt.title} - - - - ); + const { query } = useOpenInContext(); + return ( + + + {providers.chatgpt.icon} + {providers.chatgpt.title} + + + + ); }; export type OpenInClaudeProps = ComponentProps; export const OpenInClaude = (props: OpenInClaudeProps) => { - const { query } = useOpenInContext(); - return ( - - - {providers.claude.icon} - {providers.claude.title} - - - - ); + const { query } = useOpenInContext(); + return ( + + + {providers.claude.icon} + {providers.claude.title} + + + + ); }; export type OpenInT3Props = ComponentProps; export const OpenInT3 = (props: OpenInT3Props) => { - const { query } = useOpenInContext(); - return ( - - - {providers.t3.icon} - {providers.t3.title} - - - - ); + const { query } = useOpenInContext(); + return ( + + + {providers.t3.icon} + {providers.t3.title} + + + + ); }; export type OpenInSciraProps = ComponentProps; export const OpenInScira = (props: OpenInSciraProps) => { - const { query } = useOpenInContext(); - return ( - - - {providers.scira.icon} - {providers.scira.title} - - - - ); + const { query } = useOpenInContext(); + return ( + + + {providers.scira.icon} + {providers.scira.title} + + + + ); }; export type OpenInv0Props = ComponentProps; export const OpenInv0 = (props: OpenInv0Props) => { - const { query } = useOpenInContext(); - return ( - - - {providers.v0.icon} - {providers.v0.title} - - - - ); + const { query } = useOpenInContext(); + return ( + + + {providers.v0.icon} + {providers.v0.title} + + + + ); }; export type OpenInCursorProps = ComponentProps; export const OpenInCursor = (props: OpenInCursorProps) => { - const { query } = useOpenInContext(); - return ( - - - {providers.cursor.icon} - {providers.cursor.title} - - - - ); + const { query } = useOpenInContext(); + return ( + + + {providers.cursor.icon} + {providers.cursor.title} + + + + ); }; diff --git a/vite/src/components/ai-elements/panel.tsx b/vite/src/components/ai-elements/panel.tsx index 059cb7ac2..18e941c93 100644 --- a/vite/src/components/ai-elements/panel.tsx +++ b/vite/src/components/ai-elements/panel.tsx @@ -1,15 +1,15 @@ -import { cn } from "@/lib/utils"; import { Panel as PanelPrimitive } from "@xyflow/react"; import type { ComponentProps } from "react"; +import { cn } from "@/lib/utils"; type PanelProps = ComponentProps; export const Panel = ({ className, ...props }: PanelProps) => ( - + ); diff --git a/vite/src/components/ai-elements/plan.tsx b/vite/src/components/ai-elements/plan.tsx index be04d883b..bb1cc1c8e 100644 --- a/vite/src/components/ai-elements/plan.tsx +++ b/vite/src/components/ai-elements/plan.tsx @@ -1,142 +1,142 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; import { ChevronsUpDownIcon } from "lucide-react"; import type { ComponentProps } from "react"; import { createContext, useContext } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; import { Shimmer } from "./shimmer"; type PlanContextValue = { - isStreaming: boolean; + isStreaming: boolean; }; const PlanContext = createContext(null); const usePlan = () => { - const context = useContext(PlanContext); - if (!context) { - throw new Error("Plan components must be used within Plan"); - } - return context; + const context = useContext(PlanContext); + if (!context) { + throw new Error("Plan components must be used within Plan"); + } + return context; }; export type PlanProps = ComponentProps & { - isStreaming?: boolean; + isStreaming?: boolean; }; export const Plan = ({ - className, - isStreaming = false, - children, - ...props + className, + isStreaming = false, + children, + ...props }: PlanProps) => ( - - - {children} - - + + + {children} + + ); export type PlanHeaderProps = ComponentProps; export const PlanHeader = ({ className, ...props }: PlanHeaderProps) => ( - + ); export type PlanTitleProps = Omit< - ComponentProps, - "children" + ComponentProps, + "children" > & { - children: string; + children: string; }; export const PlanTitle = ({ children, ...props }: PlanTitleProps) => { - const { isStreaming } = usePlan(); + const { isStreaming } = usePlan(); - return ( - - {isStreaming ? {children} : children} - - ); + return ( + + {isStreaming ? {children} : children} + + ); }; export type PlanDescriptionProps = Omit< - ComponentProps, - "children" + ComponentProps, + "children" > & { - children: string; + children: string; }; export const PlanDescription = ({ - className, - children, - ...props + className, + children, + ...props }: PlanDescriptionProps) => { - const { isStreaming } = usePlan(); + const { isStreaming } = usePlan(); - return ( - - {isStreaming ? {children} : children} - - ); + return ( + + {isStreaming ? {children} : children} + + ); }; export type PlanActionProps = ComponentProps; export const PlanAction = (props: PlanActionProps) => ( - + ); export type PlanContentProps = ComponentProps; export const PlanContent = (props: PlanContentProps) => ( - - - + + + ); export type PlanFooterProps = ComponentProps<"div">; export const PlanFooter = (props: PlanFooterProps) => ( - + ); export type PlanTriggerProps = ComponentProps; export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => ( - - - + + + ); diff --git a/vite/src/components/ai-elements/queue.tsx b/vite/src/components/ai-elements/queue.tsx index 0c91d1300..b9c31d7cf 100644 --- a/vite/src/components/ai-elements/queue.tsx +++ b/vite/src/components/ai-elements/queue.tsx @@ -1,274 +1,274 @@ "use client"; +import { ChevronDownIcon, PaperclipIcon } from "lucide-react"; +import type { ComponentProps } from "react"; import { Button } from "@/components/ui/button"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, } from "@/components/ui/collapsible"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; -import { ChevronDownIcon, PaperclipIcon } from "lucide-react"; -import type { ComponentProps } from "react"; export type QueueMessagePart = { - type: string; - text?: string; - url?: string; - filename?: string; - mediaType?: string; + type: string; + text?: string; + url?: string; + filename?: string; + mediaType?: string; }; export type QueueMessage = { - id: string; - parts: QueueMessagePart[]; + id: string; + parts: QueueMessagePart[]; }; export type QueueTodo = { - id: string; - title: string; - description?: string; - status?: "pending" | "completed"; + id: string; + title: string; + description?: string; + status?: "pending" | "completed"; }; export type QueueItemProps = ComponentProps<"li">; export const QueueItem = ({ className, ...props }: QueueItemProps) => ( -
  • +
  • ); export type QueueItemIndicatorProps = ComponentProps<"span"> & { - completed?: boolean; + completed?: boolean; }; export const QueueItemIndicator = ({ - completed = false, - className, - ...props + completed = false, + className, + ...props }: QueueItemIndicatorProps) => ( - + ); export type QueueItemContentProps = ComponentProps<"span"> & { - completed?: boolean; + completed?: boolean; }; export const QueueItemContent = ({ - completed = false, - className, - ...props + completed = false, + className, + ...props }: QueueItemContentProps) => ( - + ); export type QueueItemDescriptionProps = ComponentProps<"div"> & { - completed?: boolean; + completed?: boolean; }; export const QueueItemDescription = ({ - completed = false, - className, - ...props + completed = false, + className, + ...props }: QueueItemDescriptionProps) => ( -
    +
    ); export type QueueItemActionsProps = ComponentProps<"div">; export const QueueItemActions = ({ - className, - ...props + className, + ...props }: QueueItemActionsProps) => ( -
    +
    ); export type QueueItemActionProps = Omit< - ComponentProps, - "variant" | "size" + ComponentProps, + "variant" | "size" >; export const QueueItemAction = ({ - className, - ...props + className, + ...props }: QueueItemActionProps) => ( - - + + + ); // QueueSectionLabel - label content with icon and count export type QueueSectionLabelProps = ComponentProps<"span"> & { - count?: number; - label: string; - icon?: React.ReactNode; + count?: number; + label: string; + icon?: React.ReactNode; }; export const QueueSectionLabel = ({ - count, - label, - icon, - className, - ...props + count, + label, + icon, + className, + ...props }: QueueSectionLabelProps) => ( - - - {icon} - - {count} {label} - - + + + {icon} + + {count} {label} + + ); // QueueSectionContent - collapsible content area export type QueueSectionContentProps = ComponentProps< - typeof CollapsibleContent + typeof CollapsibleContent >; export const QueueSectionContent = ({ - className, - ...props + className, + ...props }: QueueSectionContentProps) => ( - + ); export type QueueProps = ComponentProps<"div">; export const Queue = ({ className, ...props }: QueueProps) => ( -
    +
    ); diff --git a/vite/src/components/ai-elements/reasoning.tsx b/vite/src/components/ai-elements/reasoning.tsx index 777f23b1a..55b5ebedd 100644 --- a/vite/src/components/ai-elements/reasoning.tsx +++ b/vite/src/components/ai-elements/reasoning.tsx @@ -1,178 +1,185 @@ "use client"; import { useControllableState } from "@radix-ui/react-use-controllable-state"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; import { BrainIcon, ChevronDownIcon } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { createContext, memo, useContext, useEffect, useState } from "react"; import { Streamdown } from "streamdown"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; import { Shimmer } from "./shimmer"; type ReasoningContextValue = { - isStreaming: boolean; - isOpen: boolean; - setIsOpen: (open: boolean) => void; - duration: number | undefined; + isStreaming: boolean; + isOpen: boolean; + setIsOpen: (open: boolean) => void; + duration: number | undefined; }; const ReasoningContext = createContext(null); export const useReasoning = () => { - const context = useContext(ReasoningContext); - if (!context) { - throw new Error("Reasoning components must be used within Reasoning"); - } - return context; + const context = useContext(ReasoningContext); + if (!context) { + throw new Error("Reasoning components must be used within Reasoning"); + } + return context; }; export type ReasoningProps = ComponentProps & { - isStreaming?: boolean; - open?: boolean; - defaultOpen?: boolean; - onOpenChange?: (open: boolean) => void; - duration?: number; + isStreaming?: boolean; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + duration?: number; }; const AUTO_CLOSE_DELAY = 1000; const MS_IN_S = 1000; export const Reasoning = memo( - ({ - className, - isStreaming = false, - open, - defaultOpen = true, - onOpenChange, - duration: durationProp, - children, - ...props - }: ReasoningProps) => { - const [isOpen, setIsOpen] = useControllableState({ - prop: open, - defaultProp: defaultOpen, - onChange: onOpenChange, - }); - const [duration, setDuration] = useControllableState({ - prop: durationProp, - defaultProp: undefined, - }); + ({ + className, + isStreaming = false, + open, + defaultOpen = true, + onOpenChange, + duration: durationProp, + children, + ...props + }: ReasoningProps) => { + const [isOpen, setIsOpen] = useControllableState({ + prop: open, + defaultProp: defaultOpen, + onChange: onOpenChange, + }); + const [duration, setDuration] = useControllableState({ + prop: durationProp, + defaultProp: undefined, + }); - const [hasAutoClosed, setHasAutoClosed] = useState(false); - const [startTime, setStartTime] = useState(null); + const [hasAutoClosed, setHasAutoClosed] = useState(false); + const [startTime, setStartTime] = useState(null); - // Track duration when streaming starts and ends - useEffect(() => { - if (isStreaming) { - if (startTime === null) { - setStartTime(Date.now()); - } - } else if (startTime !== null) { - setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S)); - setStartTime(null); - } - }, [isStreaming, startTime, setDuration]); + // Track duration when streaming starts and ends + useEffect(() => { + if (isStreaming) { + if (startTime === null) { + setStartTime(Date.now()); + } + } else if (startTime !== null) { + setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S)); + setStartTime(null); + } + }, [isStreaming, startTime, setDuration]); - // Auto-open when streaming starts, auto-close when streaming ends (once only) - useEffect(() => { - if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) { - // Add a small delay before closing to allow user to see the content - const timer = setTimeout(() => { - setIsOpen(false); - setHasAutoClosed(true); - }, AUTO_CLOSE_DELAY); + // Auto-open when streaming starts, auto-close when streaming ends (once only) + useEffect(() => { + if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) { + // Add a small delay before closing to allow user to see the content + const timer = setTimeout(() => { + setIsOpen(false); + setHasAutoClosed(true); + }, AUTO_CLOSE_DELAY); - return () => clearTimeout(timer); - } - }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]); + return () => clearTimeout(timer); + } + }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed]); - const handleOpenChange = (newOpen: boolean) => { - setIsOpen(newOpen); - }; + const handleOpenChange = (newOpen: boolean) => { + setIsOpen(newOpen); + }; - return ( - - - {children} - - - ); - } + return ( + + + {children} + + + ); + }, ); -export type ReasoningTriggerProps = ComponentProps & { - getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode; +export type ReasoningTriggerProps = ComponentProps< + typeof CollapsibleTrigger +> & { + getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode; }; const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => { - if (isStreaming || duration === 0) { - return Thinking...; - } - if (duration === undefined) { - return

    Thought for a few seconds

    ; - } - return

    Thought for {duration} seconds

    ; + if (isStreaming || duration === 0) { + return Thinking...; + } + if (duration === undefined) { + return

    Thought for a few seconds

    ; + } + return

    Thought for {duration} seconds

    ; }; export const ReasoningTrigger = memo( - ({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => { - const { isStreaming, isOpen, duration } = useReasoning(); + ({ + className, + children, + getThinkingMessage = defaultGetThinkingMessage, + ...props + }: ReasoningTriggerProps) => { + const { isStreaming, isOpen, duration } = useReasoning(); - return ( - - {children ?? ( - <> - - {getThinkingMessage(isStreaming, duration)} - - - )} - - ); - } + return ( + + {children ?? ( + <> + + {getThinkingMessage(isStreaming, duration)} + + + )} + + ); + }, ); export type ReasoningContentProps = ComponentProps< - typeof CollapsibleContent + typeof CollapsibleContent > & { - children: string; + children: string; }; export const ReasoningContent = memo( - ({ className, children, ...props }: ReasoningContentProps) => ( - - {children} - - ) + ({ className, children, ...props }: ReasoningContentProps) => ( + + {children} + + ), ); Reasoning.displayName = "Reasoning"; diff --git a/vite/src/components/ai-elements/shimmer.tsx b/vite/src/components/ai-elements/shimmer.tsx index 9163aac4d..a0673212a 100644 --- a/vite/src/components/ai-elements/shimmer.tsx +++ b/vite/src/components/ai-elements/shimmer.tsx @@ -1,64 +1,64 @@ "use client"; -import { cn } from "@/lib/utils"; import { motion } from "motion/react"; import { - type CSSProperties, - type ElementType, - type JSX, - memo, - useMemo, + type CSSProperties, + type ElementType, + type JSX, + memo, + useMemo, } from "react"; +import { cn } from "@/lib/utils"; export type TextShimmerProps = { - children: string; - as?: ElementType; - className?: string; - duration?: number; - spread?: number; + children: string; + as?: ElementType; + className?: string; + duration?: number; + spread?: number; }; const ShimmerComponent = ({ - children, - as: Component = "p", - className, - duration = 2, - spread = 2, + children, + as: Component = "p", + className, + duration = 2, + spread = 2, }: TextShimmerProps) => { - const MotionComponent = motion.create( - Component as keyof JSX.IntrinsicElements - ); + const MotionComponent = motion.create( + Component as keyof JSX.IntrinsicElements, + ); - const dynamicSpread = useMemo( - () => (children?.length ?? 0) * spread, - [children, spread] - ); + const dynamicSpread = useMemo( + () => (children?.length ?? 0) * spread, + [children, spread], + ); - return ( - - {children} - - ); + return ( + + {children} + + ); }; export const Shimmer = memo(ShimmerComponent); diff --git a/vite/src/components/ai-elements/sources.tsx b/vite/src/components/ai-elements/sources.tsx index 0756664f3..477d007e2 100644 --- a/vite/src/components/ai-elements/sources.tsx +++ b/vite/src/components/ai-elements/sources.tsx @@ -1,77 +1,77 @@ "use client"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; import { BookIcon, ChevronDownIcon } from "lucide-react"; import type { ComponentProps } from "react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; export type SourcesProps = ComponentProps<"div">; export const Sources = ({ className, ...props }: SourcesProps) => ( - + ); export type SourcesTriggerProps = ComponentProps & { - count: number; + count: number; }; export const SourcesTrigger = ({ - className, - count, - children, - ...props + className, + count, + children, + ...props }: SourcesTriggerProps) => ( - - {children ?? ( - <> -

    Used {count} sources

    - - - )} -
    + + {children ?? ( + <> +

    Used {count} sources

    + + + )} +
    ); export type SourcesContentProps = ComponentProps; export const SourcesContent = ({ - className, - ...props + className, + ...props }: SourcesContentProps) => ( - + ); export type SourceProps = ComponentProps<"a">; export const Source = ({ href, title, children, ...props }: SourceProps) => ( - - {children ?? ( - <> - - {title} - - )} - + + {children ?? ( + <> + + {title} + + )} + ); diff --git a/vite/src/components/ai-elements/suggestion.tsx b/vite/src/components/ai-elements/suggestion.tsx index 9d76a8211..564b7c49a 100644 --- a/vite/src/components/ai-elements/suggestion.tsx +++ b/vite/src/components/ai-elements/suggestion.tsx @@ -1,56 +1,53 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - ScrollArea, - ScrollBar, -} from "@/components/ui/scroll-area"; -import { cn } from "@/lib/utils"; import type { ComponentProps } from "react"; +import { Button } from "@/components/ui/button"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; export type SuggestionsProps = ComponentProps; export const Suggestions = ({ - className, - children, - ...props + className, + children, + ...props }: SuggestionsProps) => ( - -
    - {children} -
    - -
    + +
    + {children} +
    + +
    ); export type SuggestionProps = Omit, "onClick"> & { - suggestion: string; - onClick?: (suggestion: string) => void; + suggestion: string; + onClick?: (suggestion: string) => void; }; export const Suggestion = ({ - suggestion, - onClick, - className, - variant = "outline", - size = "sm", - children, - ...props + suggestion, + onClick, + className, + variant = "outline", + size = "sm", + children, + ...props }: SuggestionProps) => { - const handleClick = () => { - onClick?.(suggestion); - }; + const handleClick = () => { + onClick?.(suggestion); + }; - return ( - - ); + return ( + + ); }; diff --git a/vite/src/components/ai-elements/task.tsx b/vite/src/components/ai-elements/task.tsx index eeb802c61..ff14964f0 100644 --- a/vite/src/components/ai-elements/task.tsx +++ b/vite/src/components/ai-elements/task.tsx @@ -1,87 +1,87 @@ "use client"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; import { ChevronDownIcon, SearchIcon } from "lucide-react"; import type { ComponentProps } from "react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; export type TaskItemFileProps = ComponentProps<"div">; export const TaskItemFile = ({ - children, - className, - ...props + children, + className, + ...props }: TaskItemFileProps) => ( -
    - {children} -
    +
    + {children} +
    ); export type TaskItemProps = ComponentProps<"div">; export const TaskItem = ({ children, className, ...props }: TaskItemProps) => ( -
    - {children} -
    +
    + {children} +
    ); export type TaskProps = ComponentProps; export const Task = ({ - defaultOpen = true, - className, - ...props + defaultOpen = true, + className, + ...props }: TaskProps) => ( - + ); export type TaskTriggerProps = ComponentProps & { - title: string; + title: string; }; export const TaskTrigger = ({ - children, - className, - title, - ...props + children, + className, + title, + ...props }: TaskTriggerProps) => ( - - {children ?? ( -
    - -

    {title}

    - -
    - )} -
    + + {children ?? ( +
    + +

    {title}

    + +
    + )} +
    ); export type TaskContentProps = ComponentProps; export const TaskContent = ({ - children, - className, - ...props + children, + className, + ...props }: TaskContentProps) => ( - -
    - {children} -
    -
    + +
    + {children} +
    +
    ); diff --git a/vite/src/components/ai-elements/tool.tsx b/vite/src/components/ai-elements/tool.tsx index dfdd31dda..f9543a06b 100644 --- a/vite/src/components/ai-elements/tool.tsx +++ b/vite/src/components/ai-elements/tool.tsx @@ -1,165 +1,165 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { cn } from "@/lib/utils"; import type { ToolUIPart } from "ai"; import { - CheckCircleIcon, - ChevronDownIcon, - CircleIcon, - ClockIcon, - WrenchIcon, - XCircleIcon, + CheckCircleIcon, + ChevronDownIcon, + CircleIcon, + ClockIcon, + WrenchIcon, + XCircleIcon, } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { isValidElement } from "react"; +import { Badge } from "@/components/ui/badge"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; import { CodeBlock } from "./code-block"; export type ToolProps = ComponentProps; export const Tool = ({ className, ...props }: ToolProps) => ( - + ); export type ToolHeaderProps = { - title?: string; - type: ToolUIPart["type"]; - state: ToolUIPart["state"]; - className?: string; + title?: string; + type: ToolUIPart["type"]; + state: ToolUIPart["state"]; + className?: string; }; const getStatusBadge = (status: ToolUIPart["state"]) => { - const labels: Record = { - "input-streaming": "Pending", - "input-available": "Running", - // @ts-expect-error state only available in AI SDK v6 - "approval-requested": "Awaiting Approval", - "approval-responded": "Responded", - "output-available": "Completed", - "output-error": "Error", - "output-denied": "Denied", - }; + const labels: Record = { + "input-streaming": "Pending", + "input-available": "Running", + // @ts-expect-error state only available in AI SDK v6 + "approval-requested": "Awaiting Approval", + "approval-responded": "Responded", + "output-available": "Completed", + "output-error": "Error", + "output-denied": "Denied", + }; - const icons: Record = { - "input-streaming": , - "input-available": , - // @ts-expect-error state only available in AI SDK v6 - "approval-requested": , - "approval-responded": , - "output-available": , - "output-error": , - "output-denied": , - }; + const icons: Record = { + "input-streaming": , + "input-available": , + // @ts-expect-error state only available in AI SDK v6 + "approval-requested": , + "approval-responded": , + "output-available": , + "output-error": , + "output-denied": , + }; - return ( - - {icons[status]} - {labels[status]} - - ); + return ( + + {icons[status]} + {labels[status]} + + ); }; export const ToolHeader = ({ - className, - title, - type, - state, - ...props + className, + title, + type, + state, + ...props }: ToolHeaderProps) => ( - -
    - - - {title ?? type.split("-").slice(1).join("-")} - - {getStatusBadge(state)} -
    - -
    + +
    + + + {title ?? type.split("-").slice(1).join("-")} + + {getStatusBadge(state)} +
    + +
    ); export type ToolContentProps = ComponentProps; export const ToolContent = ({ className, ...props }: ToolContentProps) => ( - + ); export type ToolInputProps = ComponentProps<"div"> & { - input: ToolUIPart["input"]; + input: ToolUIPart["input"]; }; export const ToolInput = ({ className, input, ...props }: ToolInputProps) => ( -
    -

    - Parameters -

    -
    - -
    -
    +
    +

    + Parameters +

    +
    + +
    +
    ); export type ToolOutputProps = ComponentProps<"div"> & { - output: ToolUIPart["output"]; - errorText: ToolUIPart["errorText"]; + output: ToolUIPart["output"]; + errorText: ToolUIPart["errorText"]; }; export const ToolOutput = ({ - className, - output, - errorText, - ...props + className, + output, + errorText, + ...props }: ToolOutputProps) => { - if (!(output || errorText)) { - return null; - } + if (!(output || errorText)) { + return null; + } - let Output =
    {output as ReactNode}
    ; + let Output =
    {output as ReactNode}
    ; - if (typeof output === "object" && !isValidElement(output)) { - Output = ( - - ); - } else if (typeof output === "string") { - Output = ; - } + if (typeof output === "object" && !isValidElement(output)) { + Output = ( + + ); + } else if (typeof output === "string") { + Output = ; + } - return ( -
    -

    - {errorText ? "Error" : "Result"} -

    -
    - {errorText &&
    {errorText}
    } - {Output} -
    -
    - ); + return ( +
    +

    + {errorText ? "Error" : "Result"} +

    +
    + {errorText &&
    {errorText}
    } + {Output} +
    +
    + ); }; diff --git a/vite/src/components/ai-elements/toolbar.tsx b/vite/src/components/ai-elements/toolbar.tsx index b55aa8895..8e0dcb25d 100644 --- a/vite/src/components/ai-elements/toolbar.tsx +++ b/vite/src/components/ai-elements/toolbar.tsx @@ -1,16 +1,16 @@ -import { cn } from "@/lib/utils"; import { NodeToolbar, Position } from "@xyflow/react"; import type { ComponentProps } from "react"; +import { cn } from "@/lib/utils"; type ToolbarProps = ComponentProps; export const Toolbar = ({ className, ...props }: ToolbarProps) => ( - + ); diff --git a/vite/src/components/ai-elements/web-preview.tsx b/vite/src/components/ai-elements/web-preview.tsx index 8f0ab5a53..0d528f32c 100644 --- a/vite/src/components/ai-elements/web-preview.tsx +++ b/vite/src/components/ai-elements/web-preview.tsx @@ -1,263 +1,263 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Input } from "@/components/ui/input"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; import { ChevronDownIcon } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { createContext, useContext, useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Input } from "@/components/ui/input"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; export type WebPreviewContextValue = { - url: string; - setUrl: (url: string) => void; - consoleOpen: boolean; - setConsoleOpen: (open: boolean) => void; + url: string; + setUrl: (url: string) => void; + consoleOpen: boolean; + setConsoleOpen: (open: boolean) => void; }; const WebPreviewContext = createContext(null); const useWebPreview = () => { - const context = useContext(WebPreviewContext); - if (!context) { - throw new Error("WebPreview components must be used within a WebPreview"); - } - return context; + const context = useContext(WebPreviewContext); + if (!context) { + throw new Error("WebPreview components must be used within a WebPreview"); + } + return context; }; export type WebPreviewProps = ComponentProps<"div"> & { - defaultUrl?: string; - onUrlChange?: (url: string) => void; + defaultUrl?: string; + onUrlChange?: (url: string) => void; }; export const WebPreview = ({ - className, - children, - defaultUrl = "", - onUrlChange, - ...props + className, + children, + defaultUrl = "", + onUrlChange, + ...props }: WebPreviewProps) => { - const [url, setUrl] = useState(defaultUrl); - const [consoleOpen, setConsoleOpen] = useState(false); + const [url, setUrl] = useState(defaultUrl); + const [consoleOpen, setConsoleOpen] = useState(false); - const handleUrlChange = (newUrl: string) => { - setUrl(newUrl); - onUrlChange?.(newUrl); - }; + const handleUrlChange = (newUrl: string) => { + setUrl(newUrl); + onUrlChange?.(newUrl); + }; - const contextValue: WebPreviewContextValue = { - url, - setUrl: handleUrlChange, - consoleOpen, - setConsoleOpen, - }; + const contextValue: WebPreviewContextValue = { + url, + setUrl: handleUrlChange, + consoleOpen, + setConsoleOpen, + }; - return ( - -
    - {children} -
    -
    - ); + return ( + +
    + {children} +
    +
    + ); }; export type WebPreviewNavigationProps = ComponentProps<"div">; export const WebPreviewNavigation = ({ - className, - children, - ...props + className, + children, + ...props }: WebPreviewNavigationProps) => ( -
    - {children} -
    +
    + {children} +
    ); export type WebPreviewNavigationButtonProps = ComponentProps & { - tooltip?: string; + tooltip?: string; }; export const WebPreviewNavigationButton = ({ - onClick, - disabled, - tooltip, - children, - ...props + onClick, + disabled, + tooltip, + children, + ...props }: WebPreviewNavigationButtonProps) => ( - - - - - - -

    {tooltip}

    -
    -
    -
    + + + + + + +

    {tooltip}

    +
    +
    +
    ); export type WebPreviewUrlProps = ComponentProps; export const WebPreviewUrl = ({ - value, - onChange, - onKeyDown, - ...props + value, + onChange, + onKeyDown, + ...props }: WebPreviewUrlProps) => { - const { url, setUrl } = useWebPreview(); - const [inputValue, setInputValue] = useState(url); + const { url, setUrl } = useWebPreview(); + const [inputValue, setInputValue] = useState(url); - // Sync input value with context URL when it changes externally - useEffect(() => { - setInputValue(url); - }, [url]); + // Sync input value with context URL when it changes externally + useEffect(() => { + setInputValue(url); + }, [url]); - const handleChange = (event: React.ChangeEvent) => { - setInputValue(event.target.value); - onChange?.(event); - }; + const handleChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + onChange?.(event); + }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === "Enter") { - const target = event.target as HTMLInputElement; - setUrl(target.value); - } - onKeyDown?.(event); - }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + const target = event.target as HTMLInputElement; + setUrl(target.value); + } + onKeyDown?.(event); + }; - return ( - - ); + return ( + + ); }; export type WebPreviewBodyProps = ComponentProps<"iframe"> & { - loading?: ReactNode; + loading?: ReactNode; }; export const WebPreviewBody = ({ - className, - loading, - src, - ...props + className, + loading, + src, + ...props }: WebPreviewBodyProps) => { - const { url } = useWebPreview(); + const { url } = useWebPreview(); - return ( -
    -