Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions bifrost/lib/clients/jawnTypes/private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ export interface paths {
delete: operations["RemoveOrgMember"];
patch: operations["UpdateOrgMemberRole"];
};
"/v1/admin/org/{orgId}/gateway-discount": {
patch: operations["UpdateGatewayDiscount"];
};
"/v1/admin/org/{orgId}/delete": {
post: operations["DeleteOrg"];
};
Expand Down Expand Up @@ -19438,6 +19441,7 @@ export interface operations {
email: string;
id: string;
})[];
gateway_discount_enabled: boolean;
subscription_status: string | null;
stripe_subscription_id: string | null;
stripe_customer_id: string | null;
Expand Down Expand Up @@ -19529,6 +19533,28 @@ export interface operations {
};
};
};
UpdateGatewayDiscount: {
parameters: {
path: {
orgId: string;
};
};
requestBody: {
content: {
"application/json": {
enabled: boolean;
};
};
};
responses: {
/** @description Ok */
200: {
content: {
"application/json": components["schemas"]["Result_null.string_"];
};
};
};
};
DeleteOrg: {
parameters: {
path: {
Expand Down
26 changes: 25 additions & 1 deletion valhalla/jawn/src/controllers/private/adminController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,7 @@ export class AdminController extends Controller {
stripe_customer_id: string | null;
stripe_subscription_id: string | null;
subscription_status: string | null;
gateway_discount_enabled: boolean;
members: {
id: string;
email: string;
Expand Down Expand Up @@ -1060,6 +1061,7 @@ export class AdminController extends Controller {
SELECT
o.id, o.name, o.created_at, o.owner, o.tier,
o.stripe_customer_id, o.stripe_subscription_id, o.subscription_status,
o.gateway_discount_enabled,
m.match_score,
json_agg(
json_build_object(
Expand All @@ -1076,7 +1078,8 @@ export class AdminController extends Controller {
LEFT JOIN organization_member om ON o.id = om.organization
LEFT JOIN auth.users u ON om.member = u.id
GROUP BY o.id, o.name, o.created_at, o.owner, o.tier,
o.stripe_customer_id, o.stripe_subscription_id, o.subscription_status, m.match_score
o.stripe_customer_id, o.stripe_subscription_id, o.subscription_status,
o.gateway_discount_enabled, m.match_score
ORDER BY m.match_score ASC, o.name ASC
LIMIT $2 OFFSET $3
`;
Expand Down Expand Up @@ -1117,6 +1120,7 @@ export class AdminController extends Controller {
stripe_customer_id: string | null;
stripe_subscription_id: string | null;
subscription_status: string | null;
gateway_discount_enabled: boolean;
match_score: number;
members: {
id: string;
Expand Down Expand Up @@ -1348,6 +1352,26 @@ export class AdminController extends Controller {
return ok(null);
}

@Patch("/org/{orgId}/gateway-discount")
public async updateGatewayDiscount(
@Request() request: JawnAuthenticatedRequest,
@Path() orgId: string,
@Body() body: { enabled: boolean }
): Promise<Result<null, string>> {
await authCheckThrow(request.authParams.userId);

const { error } = await dbExecute(
`UPDATE organization SET gateway_discount_enabled = $1 WHERE id = $2`,
[body.enabled, orgId]
);

if (error) {
return err(error);
}

return ok(null);
}

@Post("/org/{orgId}/delete")
public async deleteOrg(
@Request() request: JawnAuthenticatedRequest,
Expand Down
33 changes: 33 additions & 0 deletions valhalla/jawn/src/tsoa-build/private/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20629,6 +20629,39 @@ export function RegisterRoutes(app: Router) {
}
});
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
const argsAdminController_updateGatewayDiscount: Record<string, TsoaRoute.ParameterSchema> = {
request: {"in":"request","name":"request","required":true,"dataType":"object"},
orgId: {"in":"path","name":"orgId","required":true,"dataType":"string"},
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"enabled":{"dataType":"boolean","required":true}}},
};
app.patch('/v1/admin/org/:orgId/gateway-discount',
authenticateMiddleware([{"api_key":[]}]),
...(fetchMiddlewares<RequestHandler>(AdminController)),
...(fetchMiddlewares<RequestHandler>(AdminController.prototype.updateGatewayDiscount)),

async function AdminController_updateGatewayDiscount(request: ExRequest, response: ExResponse, next: any) {

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

let validatedArgs: any[] = [];
try {
validatedArgs = templateService.getValidatedArgs({ args: argsAdminController_updateGatewayDiscount, request, response });

const controller = new AdminController();

await templateService.apiHandler({
methodName: 'updateGatewayDiscount',
controller,
response,
next,
validatedArgs,
successStatus: undefined,
});
} catch (err) {
return next(err);
}
});
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
const argsAdminController_deleteOrg: Record<string, TsoaRoute.ParameterSchema> = {
request: {"in":"request","name":"request","required":true,"dataType":"object"},
orgId: {"in":"path","name":"orgId","required":true,"dataType":"string"},
Expand Down
57 changes: 57 additions & 0 deletions valhalla/jawn/src/tsoa-build/private/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -56573,6 +56573,9 @@
},
"type": "array"
},
"gateway_discount_enabled": {
"type": "boolean"
},
"subscription_status": {
"type": "string",
"nullable": true
Expand Down Expand Up @@ -56603,6 +56606,7 @@
},
"required": [
"members",
"gateway_discount_enabled",
"subscription_status",
"stripe_subscription_id",
"stripe_customer_id",
Expand Down Expand Up @@ -56894,6 +56898,59 @@
}
}
},
"/v1/admin/org/{orgId}/gateway-discount": {
"patch": {
"operationId": "UpdateGatewayDiscount",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Result_null.string_"
}
}
}
}
},
"tags": [
"Admin"
],
"security": [
{
"api_key": []
}
],
"parameters": [
{
"in": "path",
"name": "orgId",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
}
}
}
}
}
},
"/v1/admin/org/{orgId}/delete": {
"post": {
"operationId": "DeleteOrg",
Expand Down
118 changes: 118 additions & 0 deletions web/components/templates/admin/orgSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";

const OrgSearch = () => {
const router = useRouter();
Expand Down Expand Up @@ -1065,6 +1066,15 @@ const OrgTableRow = ({
)}
</div>

{/* Gateway Discount */}
<div className="border-t border-border pt-2">
<GatewayDiscountSection
orgId={org.id}
orgName={org.name}
gatewayDiscountEnabled={org.gateway_discount_enabled}
/>
</div>

{/* Feature Flags */}
<div className="border-t border-border pt-2">
<FeatureFlagsSection orgId={org.id} orgName={org.name} />
Expand Down Expand Up @@ -1704,4 +1714,112 @@ const FeatureFlagsSection = ({
);
};

// Gateway Discount Section Component
const GatewayDiscountSection = ({
orgId,
orgName,
gatewayDiscountEnabled,
}: {
orgId: string;
orgName: string;
gatewayDiscountEnabled: boolean;
}) => {
const queryClient = useQueryClient();
const { setNotification } = useNotification();
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [pendingValue, setPendingValue] = useState<boolean | null>(null);

// Update gateway discount mutation
const updateGatewayDiscountMutation = useMutation({
mutationFn: async (enabled: boolean) => {
const jawn = getJawnClient();
const { error } = await jawn.PATCH("/v1/admin/org/{orgId}/gateway-discount", {
params: { path: { orgId } },
body: { enabled },
});
if (error) throw new Error(error);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["orgSearchFast"] });
setNotification("Gateway discount updated successfully", "success");
setConfirmDialogOpen(false);
setPendingValue(null);
},
onError: (error: any) => {
setNotification(
error.message || "Failed to update gateway discount",
"error",
);
setPendingValue(null);
},
});

const handleToggle = (checked: boolean) => {
setPendingValue(checked);
setConfirmDialogOpen(true);
};

const confirmToggle = () => {
if (pendingValue !== null) {
updateGatewayDiscountMutation.mutate(pendingValue);
}
};

return (
<div className="flex w-full flex-col gap-2">
<div className="flex items-center justify-between">
<Small className="font-medium">Gateway Discount</Small>
<div className="flex items-center gap-2">
<Switch
checked={gatewayDiscountEnabled}
onCheckedChange={handleToggle}
disabled={updateGatewayDiscountMutation.isPending}
/>
<Small className="text-muted-foreground">
{gatewayDiscountEnabled ? "Enabled" : "Disabled"}
</Small>
</div>
</div>

{/* Confirmation Dialog */}
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: When dialog closes via outside click, ESC key, or X button, pendingValue isn't reset, so reopening shows stale confirmation text

Suggested change
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<Dialog open={confirmDialogOpen} onOpenChange={(open) => {
setConfirmDialogOpen(open);
if (!open) setPendingValue(null);
}}>
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/components/templates/admin/orgSearch.tsx
Line: 1785:1785

Comment:
**logic:** When dialog closes via outside click, ESC key, or X button, `pendingValue` isn't reset, so reopening shows stale confirmation text

```suggestion
      <Dialog open={confirmDialogOpen} onOpenChange={(open) => {
        setConfirmDialogOpen(open);
        if (!open) setPendingValue(null);
      }}>
```

How can I resolve this? If you propose a fix, please make it concise.

<DialogContent>
<DialogHeader>
<DialogTitle>Update Gateway Discount</DialogTitle>
<DialogDescription>
Are you sure you want to {pendingValue ? "enable" : "disable"} gateway
discount for <span className="font-medium">{orgName}</span>?
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setConfirmDialogOpen(false);
setPendingValue(null);
}}
disabled={updateGatewayDiscountMutation.isPending}
>
Cancel
</Button>
<Button
onClick={confirmToggle}
disabled={updateGatewayDiscountMutation.isPending}
>
{updateGatewayDiscountMutation.isPending ? (
<>
<Loader2 size={16} className="mr-2 animate-spin" />
Updating...
</>
) : (
"Confirm"
)}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
};

export default OrgSearch;
Loading
Loading