11import { conform , list , requestIntent , useFieldList , useForm } from "@conform-to/react" ;
22import { parse } from "@conform-to/zod" ;
33import { Form , useActionData , type MetaFunction } from "@remix-run/react" ;
4- import { json , type ActionFunction , type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
4+ import { json } from "@remix-run/server-runtime" ;
55import { tryCatch } from "@trigger.dev/core" ;
66import { Fragment , useEffect , useRef , useState } from "react" ;
77import { redirect , typedjson , useTypedLoaderData } from "remix-typedjson" ;
@@ -25,11 +25,11 @@ import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/Page
2525import { Paragraph } from "~/components/primitives/Paragraph" ;
2626import { TextLink } from "~/components/primitives/TextLink" ;
2727import { InfoIconTooltip } from "~/components/primitives/Tooltip" ;
28- import { prisma } from "~/db.server" ;
28+ import { $replica , prisma } from "~/db.server" ;
2929import { featuresForRequest } from "~/features.server" ;
3030import { redirectWithErrorMessage , redirectWithSuccessMessage } from "~/models/message.server" ;
3131import { getBillingAlerts , getCurrentPlan , setBillingAlert } from "~/services/platform.v3.server" ;
32- import { requireUserId } from "~/services/session.server " ;
32+ import { dashboardAction , dashboardLoader } from "~/services/routeBuilders/dashboardBuilder " ;
3333import { formatCurrency , formatNumber } from "~/utils/numberFormatter" ;
3434import {
3535 docsPath ,
@@ -48,44 +48,59 @@ export const meta: MetaFunction = () => {
4848 ] ;
4949} ;
5050
51- export async function loader ( { params, request } : LoaderFunctionArgs ) {
52- const userId = await requireUserId ( request ) ;
53- const { organizationSlug } = OrganizationParamsSchema . parse ( params ) ;
51+ async function resolveOrgIdFromSlug ( slug : string ) : Promise < string | null > {
52+ const org = await $replica . organization . findFirst ( { where : { slug } , select : { id : true } } ) ;
53+ return org ?. id ?? null ;
54+ }
5455
55- const { isManagedCloud } = featuresForRequest ( request ) ;
56- if ( ! isManagedCloud ) {
57- return redirect ( organizationPath ( { slug : organizationSlug } ) ) ;
58- }
56+ export const loader = dashboardLoader (
57+ {
58+ params : OrganizationParamsSchema ,
59+ context : async ( params ) => {
60+ const organizationId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
61+ return organizationId ? { organizationId } : { } ;
62+ } ,
63+ authorization : { action : "manage" , resource : { type : "billing" } } ,
64+ } ,
65+ async ( { params, request, user } ) => {
66+ const userId = user . id ;
67+ const { organizationSlug } = params ;
5968
60- const organization = await prisma . organization . findFirst ( {
61- where : { slug : organizationSlug , members : { some : { userId } } } ,
62- } ) ;
69+ const { isManagedCloud } = featuresForRequest ( request ) ;
70+ if ( ! isManagedCloud ) {
71+ return redirect ( organizationPath ( { slug : organizationSlug } ) ) ;
72+ }
6373
64- if ( ! organization ) {
65- throw new Response ( null , { status : 404 , statusText : "Organization not found" } ) ;
66- }
74+ const organization = await prisma . organization . findFirst ( {
75+ where : { slug : organizationSlug , members : { some : { userId } } } ,
76+ } ) ;
6777
68- const currentPlan = await getCurrentPlan ( organization . id ) ;
69- if ( currentPlan ?. v3Subscription ?. showSelfServe === false ) {
70- return redirect ( v3BillingPath ( { slug : organizationSlug } ) ) ;
71- }
78+ if ( ! organization ) {
79+ throw new Response ( null , { status : 404 , statusText : "Organization not found" } ) ;
80+ }
7281
73- const [ error , alerts ] = await tryCatch ( getBillingAlerts ( organization . id ) ) ;
74- if ( error ) {
75- throw new Response ( null , { status : 404 , statusText : `Billing alerts error: ${ error } ` } ) ;
76- }
82+ const currentPlan = await getCurrentPlan ( organization . id ) ;
83+ if ( currentPlan ?. v3Subscription ?. showSelfServe === false ) {
84+ return redirect ( v3BillingPath ( { slug : organizationSlug } ) ) ;
85+ }
7786
78- if ( ! alerts ) {
79- throw new Response ( null , { status : 404 , statusText : "Billing alerts not found" } ) ;
80- }
87+ const [ error , alerts ] = await tryCatch ( getBillingAlerts ( organization . id ) ) ;
88+ if ( error ) {
89+ throw new Response ( null , { status : 404 , statusText : `Billing alerts error: ${ error } ` } ) ;
90+ }
8191
82- return typedjson ( {
83- alerts : {
84- ...alerts ,
85- amount : alerts . amount / 100 ,
86- } ,
87- } ) ;
88- }
92+ if ( ! alerts ) {
93+ throw new Response ( null , { status : 404 , statusText : "Billing alerts not found" } ) ;
94+ }
95+
96+ return typedjson ( {
97+ alerts : {
98+ ...alerts ,
99+ amount : alerts . amount / 100 ,
100+ } ,
101+ } ) ;
102+ }
103+ ) ;
89104
90105const schema = z . object ( {
91106 amount : z
@@ -110,66 +125,76 @@ const schema = z.object({
110125 } , z . coerce . number ( ) . array ( ) . nonempty ( "At least one alert level is required" ) ) ,
111126} ) ;
112127
113- export const action : ActionFunction = async ( { request, params } ) => {
114- const userId = await requireUserId ( request ) ;
115- const { organizationSlug } = OrganizationParamsSchema . parse ( params ) ;
128+ export const action = dashboardAction (
129+ {
130+ params : OrganizationParamsSchema ,
131+ context : async ( params ) => {
132+ const organizationId = await resolveOrgIdFromSlug ( params . organizationSlug ) ;
133+ return organizationId ? { organizationId } : { } ;
134+ } ,
135+ authorization : { action : "manage" , resource : { type : "billing" } } ,
136+ } ,
137+ async ( { request, params, user } ) => {
138+ const userId = user . id ;
139+ const { organizationSlug } = params ;
116140
117- const organization = await prisma . organization . findFirst ( {
118- where : { slug : organizationSlug , members : { some : { userId } } } ,
119- } ) ;
141+ const organization = await prisma . organization . findFirst ( {
142+ where : { slug : organizationSlug , members : { some : { userId } } } ,
143+ } ) ;
120144
121- if ( ! organization ) {
122- return redirectWithErrorMessage (
123- v3BillingPath ( { slug : organizationSlug } ) ,
124- request ,
125- "You are not authorized to update billing alerts"
126- ) ;
127- }
145+ if ( ! organization ) {
146+ return redirectWithErrorMessage (
147+ v3BillingPath ( { slug : organizationSlug } ) ,
148+ request ,
149+ "You are not authorized to update billing alerts"
150+ ) ;
151+ }
128152
129- const currentPlan = await getCurrentPlan ( organization . id ) ;
130- if ( currentPlan ?. v3Subscription ?. showSelfServe === false ) {
131- return redirect ( v3BillingPath ( { slug : organizationSlug } ) ) ;
132- }
153+ const currentPlan = await getCurrentPlan ( organization . id ) ;
154+ if ( currentPlan ?. v3Subscription ?. showSelfServe === false ) {
155+ return redirect ( v3BillingPath ( { slug : organizationSlug } ) ) ;
156+ }
133157
134- const formData = await request . formData ( ) ;
135- const submission = parse ( formData , { schema } ) ;
158+ const formData = await request . formData ( ) ;
159+ const submission = parse ( formData , { schema } ) ;
136160
137- if ( ! submission . value || submission . intent !== "submit" ) {
138- return json ( submission ) ;
139- }
161+ if ( ! submission . value || submission . intent !== "submit" ) {
162+ return json ( submission ) ;
163+ }
140164
141- try {
142- const [ error , updatedAlert ] = await tryCatch (
143- setBillingAlert ( organization . id , {
144- ...submission . value ,
145- amount : submission . value . amount * 100 ,
146- } )
147- ) ;
148- if ( error ) {
149- return redirectWithErrorMessage (
150- v3BillingAlertsPath ( { slug : organizationSlug } ) ,
151- request ,
152- "Failed to update billing alert"
165+ try {
166+ const [ error , updatedAlert ] = await tryCatch (
167+ setBillingAlert ( organization . id , {
168+ ...submission . value ,
169+ amount : submission . value . amount * 100 ,
170+ } )
153171 ) ;
154- }
172+ if ( error ) {
173+ return redirectWithErrorMessage (
174+ v3BillingAlertsPath ( { slug : organizationSlug } ) ,
175+ request ,
176+ "Failed to update billing alert"
177+ ) ;
178+ }
155179
156- if ( ! updatedAlert ) {
157- return redirectWithErrorMessage (
180+ if ( ! updatedAlert ) {
181+ return redirectWithErrorMessage (
182+ v3BillingAlertsPath ( { slug : organizationSlug } ) ,
183+ request ,
184+ "Failed to update billing alert"
185+ ) ;
186+ }
187+
188+ return redirectWithSuccessMessage (
158189 v3BillingAlertsPath ( { slug : organizationSlug } ) ,
159190 request ,
160- "Failed to update billing alert"
191+ "Billing alert updated "
161192 ) ;
193+ } catch ( error : any ) {
194+ return json ( { errors : { body : error . message } } , { status : 400 } ) ;
162195 }
163-
164- return redirectWithSuccessMessage (
165- v3BillingAlertsPath ( { slug : organizationSlug } ) ,
166- request ,
167- "Billing alert updated"
168- ) ;
169- } catch ( error : any ) {
170- return json ( { errors : { body : error . message } } , { status : 400 } ) ;
171196 }
172- } ;
197+ ) ;
173198
174199export default function Page ( ) {
175200 const { alerts } = useTypedLoaderData < typeof loader > ( ) ;
0 commit comments