Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9336f87
refactor: volume based routing ui
Jun 10, 2026
70a0ee3
fix: playwright tests
Jun 11, 2026
9b6efd3
Merge branch 'main' into volume-routing-revamp
Muditbhatia12 Jun 11, 2026
89c56fe
fix: playwright tests
Jun 11, 2026
2555865
Merge branch 'volume-routing-revamp' of https://github.com/juspay/hyp…
Jun 11, 2026
4316e2d
chore: addressed comments
Jun 15, 2026
65c81f6
Merge branch 'main' of https://github.com/juspay/hyperswitch-control-…
Jun 15, 2026
183599a
fix: alignment of chips
Jun 15, 2026
bbcda28
Merge branch 'main' of https://github.com/juspay/hyperswitch-control-…
Jun 15, 2026
e38ff2c
chore: addressed comments
Jun 16, 2026
cf571d1
Merge branch 'main' into volume-routing-revamp
Muditbhatia12 Jun 17, 2026
7ee058f
Merge branch 'main' into volume-routing-revamp
Muditbhatia12 Jun 22, 2026
a59d540
Merge branch 'main' of https://github.com/juspay/hyperswitch-control-…
Jun 23, 2026
77953b6
test: update visual tests
prajwalnl Jun 23, 2026
b8ed146
Merge branch 'main' of https://github.com/juspay/hyperswitch-control-…
Jun 23, 2026
d3d2177
fix: conflicts
Jun 23, 2026
38e0a98
Merge branch 'volume-routing-revamp' of https://github.com/juspay/hyp…
Jun 23, 2026
6a084b8
test: fix visual tests
prajwalnl Jun 23, 2026
ac7e991
Merge branch 'main' into volume-routing-revamp
prajwalnl Jun 23, 2026
25bcb31
test: fix failing tests
prajwalnl Jun 23, 2026
a1639ee
Merge branch 'main' into volume-routing-revamp
Muditbhatia12 Jun 23, 2026
b3db235
Merge branch 'main' into volume-routing-revamp
Muditbhatia12 Jun 23, 2026
dec816c
Merge branch 'main' into volume-routing-revamp
Muditbhatia12 Jun 23, 2026
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
13 changes: 2 additions & 11 deletions playwright-tests/e2e/6-workflow/PaymentRouting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ test.describe("Volume based routing", () => {
await homePage.routing.click();
await paymentRouting.volumeBasedRoutingSetupButton.click();

await expect(paymentRouting.noConnectorsMessage).toContainText(
"Please configure at least 1 connector",
);
await expect(paymentRouting.noProcessorFoundMessage).toBeVisible();
});

test("should display all elements in volume based routing page", async ({
Expand Down Expand Up @@ -405,9 +403,7 @@ test.describe("Payment default fallback", () => {
await homePage.routing.click();
await paymentRouting.defaultFallbackManageButton.click();

await expect(paymentRouting.noConnectorsMessageLarge).toContainText(
"Please connect at least 1 connector",
);
await expect(paymentRouting.noProcessorFoundMessage).toBeVisible();
});

test("should display connected connectors in the list", async ({
Expand Down Expand Up @@ -754,11 +750,6 @@ test.describe("Routing list - Configuration History", () => {

await expect(page.getByText("Configuration NameActivate")).toBeVisible();
await expect(page.getByText("DescriptionThis is a volume")).toBeVisible();
await expect(
page.getByText(
"Volume Based Configuration is helpful when you want a specific traffic distribution for each of the configured connectors. For eg: Stripe (70%), Adyen (20%), Checkout (10%).",
),
).toBeVisible();
await expect(page.getByText("stripe_test_1")).toBeVisible();

const activateBtn = page
Expand Down
8 changes: 2 additions & 6 deletions playwright-tests/e2e/6-workflow/PayoutRouting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ test.describe("Volume based payout routing", () => {
await homePage.payoutRouting.click();
await payoutRouting.volumeBasedRoutingSetupButton.click();

await expect(payoutRouting.noConnectorsMessage).toContainText(
"Please configure at least 1 connector",
);
await expect(payoutRouting.noProcessorFoundMessage).toBeVisible();
});

test("should display all elements in volume based payout routing page", async ({
Expand Down Expand Up @@ -370,9 +368,7 @@ test.describe("Payout default fallback", () => {
await homePage.payoutRouting.click();
await payoutRouting.defaultFallbackManageButton.click();

await expect(payoutRouting.noConnectorsMessageLarge).toContainText(
"Please connect at least 1 connector",
);
await expect(payoutRouting.noProcessorFoundMessage).toBeVisible();
});

test("should display connected payout connectors in the list", async ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export class PaymentRouting {
return this.page.locator('[class="px-3 text-2xl mt-32 "]');
}

get noProcessorFoundMessage(): Locator {
return this.page.getByText("No Processor Found");
}

get configurationHistoryTab(): Locator {
return this.page.getByRole("tab", { name: "Configuration History" });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class VolumeBasedConfiguration {
}

get connectorDropdown(): Locator {
return this.page.locator('[data-value="addProcessors"]');
return this.page.getByRole("button", { name: "Select Processors" });
}

get configurationNameInput(): Locator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export class PayoutRouting {
return this.page.locator('[class="px-3 text-2xl mt-32 "]');
}

get noProcessorFoundMessage(): Locator {
return this.page.getByText("No Processor Found");
}

get configurationHistoryTab(): Locator {
return this.page.getByRole("tab", { name: "Configuration History" });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
open VerticalStepIndicatorTypes
open IntelligentRoutingTypes

let dataSource = [Historical, Realtime]
Expand All @@ -12,7 +11,7 @@ let dataTypeVariantToString = dataType =>
| Realtime => "Realtime Data"
}

let sections = [
let sections: array<VerticalStepIndicatorTypes.section> = [
{
id: (#analyze: sections :> string),
name: "Choose Your Data Source",
Expand Down
128 changes: 66 additions & 62 deletions src/components/priority-logics/AddPLGateway.res
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type gateway = RoutingTypes.volumeSplitConnectorSelectionData
open Typography

module GatewayView = {
@react.component
Expand Down Expand Up @@ -152,78 +153,81 @@ let make = (
}

if isExpanded {
<div className="flex flex-row ml-2">
<RenderIf condition={!isFirst}>
<div className="w-8 h-10 border-jp-gray-700 ml-10 border-dashed border-b border-l " />
</RenderIf>
<div className="flex flex-col gap-6 mt-6 mb-4 pt-0.5">
<div className="flex flex-wrap gap-4">
<div className="flex">
<SelectBoxAdapter.BaseDropdown
allowMultiSelect=true
buttonText=dropDownButtonText
buttonType=Button.SecondaryFilled
hideMultiSelectButtons=true
customButtonStyle="bg-white dark:bg-jp-gray-darkgray_background !w-full"
input
options={gatewayOptions}
fixedDropDownDirection=SelectBox.TopRight
searchable=true
defaultLeftIcon={FontAwesome("plus")}
maxHeight="max-h-full sm:max-h-64"
/>
<span className="text-lg text-red-500 ml-1"> {React.string("*")} </span>
</div>
<div className="flex flex-col gap-6 w-full">
<SelectBoxAdapter.BaseDropdown
allowMultiSelect=true
buttonText=dropDownButtonText
buttonType=Button.SecondaryFilled
hideMultiSelectButtons=true
showSelectionAsChips=true
showAllSelectedOptions=false
customButtonStyle="bg-nd_gray-0 !w-full"
input
options={gatewayOptions}
fixedDropDownDirection=TopRight
searchable=true
maxHeight="max-h-full sm:max-h-64"
/>
<RenderIf condition={selectedOptions->LogicUtils.isNonEmptyArray}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
{selectedOptions
->Array.mapWithIndex((item, i) => {
let key = Int.toString(i + 1)
let connectorLabel = gateWayName(item.connector.merchant_connector_id).connector_label

{
<div className="flex flex-row" key>
<div
className="w-min flex flex-row items-center justify-around gap-2 h-10 rounded-md border border-jp-gray-500 dark:border-jp-gray-960
text-jp-gray-900 hover:text-opacity-100 dark:text-jp-gray-text_darktheme dark:hover:text-jp-gray-text_darktheme
dark:hover:text-opacity-75 text-opacity-50 hover:text-jp-gray-900 bg-gradient-to-b
from-jp-gray-250 to-jp-gray-200 dark:from-jp-gray-950 dark:to-jp-gray-950
dark:text-opacity-50 focus:outline-none px-1 ">
<NewThemeUtils.Badge number={i + 1} />
<div>
{gateWayName(
item.connector.merchant_connector_id,
).connector_label->React.string}
</div>
<Icon
name="close"
size=10
className="mr-2 cursor-pointer "
onClick={ev => {
ev->ReactEvent.Mouse.stopPropagation
removeItem(i)
}}
<div
key
className="flex items-center h-10 bg-nd_gray-0 border border-nd_gray-300 rounded-10-px overflow-hidden">
<div className="flex items-center gap-2 pl-3.5 pr-2 flex-1 min-w-0">
<GatewayIcon
gateway={item.connector.connector->String.toUpperCase}
className="w-6 h-6 shrink-0"
/>
<div className="flex-1 min-w-0">
<ToolTip
description=connectorLabel
toolTipPosition=Top
toolTipFor={<p className={`${body.md.medium} text-nd_gray-600 truncate w-full`}>
{connectorLabel->React.string}
</p>}
/>
<RenderIf condition={isDistribute && selectedOptions->Array.length > 0}>
{<>
<input
className="w-10 text-right outline-none bg-white dark:bg-jp-gray-970 px-1 border border-jp-gray-300 dark:border-jp-gray-850 rounded-md"
name=key
onChange={ev => {
let val = ReactEvent.Form.target(ev)["value"]
updatePercentage(item, val->Int.fromString->Option.getOr(0))
}}
value={item.split->Int.toString}
type_="text"
inputMode="text"
/>
<div> {React.string("%")} </div>
</>}
</RenderIf>
</div>
</div>
}
<div className="flex items-center gap-3 pr-3 shrink-0">
<RenderIf condition={isDistribute}>
<div
className="flex items-center gap-2.5 bg-nd_gray-0 border border-nd_gray-200 rounded px-2 py-0.5">
<input
className={`w-6 text-center outline-none bg-transparent ${body.md.medium} text-nd_gray-700`}
name=key
onChange={ev => {
let val = ReactEvent.Form.target(ev)["value"]
updatePercentage(item, val->Int.fromString->Option.getOr(0))
}}
value={item.split->Int.toString}
type_="text"
inputMode="text"
/>
<span className={`${body.md.medium} text-nd_gray-600`}>
{React.string("%")}
</span>
</div>
</RenderIf>
<Icon
name="nd-cross"
size=16
className="cursor-pointer text-nd_gray-400"
onClick={ev => {
ev->ReactEvent.Mouse.stopPropagation
removeItem(i)
}}
/>
</div>
</div>
})
->React.array}
</div>
</div>
</RenderIf>
</div>
} else {
<GatewayView gateways=selectedOptions />
Expand Down
8 changes: 4 additions & 4 deletions src/screens/Routing/BasicDetailsForm.res
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ module BusinessProfileInp = {
~placeholder="",
)
)}
fieldWrapperClass="p-0"
/>
}
}
Expand Down Expand Up @@ -87,8 +88,7 @@ let make = (
None
}, [])

<div
className={` mb-6 p-4 bg-white dark:bg-jp-gray-lightgray_background rounded-md border border-jp-gray-600 dark:border-jp-gray-850`}>
<div className="w-full">
{if formState === ViewConfig {
<div>
<div className="flex flex-row justify-between gap-4">
Expand Down Expand Up @@ -153,9 +153,9 @@ let make = (
routingType
/>
</RenderIf>
<FieldRenderer field=configurationNameInput />
<FieldRenderer field=configurationNameInput fieldWrapperClass="p-0" />
<RenderIf condition=showDescription>
<FieldRenderer field=descriptionInput />
<FieldRenderer field=descriptionInput fieldWrapperClass="p-0 flex flex-col" />
</RenderIf>
</div>
</div>
Expand Down
32 changes: 28 additions & 4 deletions src/screens/Routing/DefaultRouting/DefaultRouting.res
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ let make = (~urlEntityName, ~baseUrlForRedirection, ~connectorVariant) => {
let fetchDetails = useGetMethod()
let showPopUp = PopUpState.useShowPopUp()
let {profileId} = React.useContext(UserInfoProvider.defaultContext).getCommonSessionDetails()
let businessProfileRecoilVal =
HyperswitchAtom.businessProfileFromIdAtomInterface->Recoil.useRecoilValueFromAtom
let (profile, setProfile) = React.useState(_ => profileId)
let showToast = ToastState.useShowToast()
let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading)
let (gateways, setGateways) = React.useState(() => [])
Expand All @@ -24,10 +27,15 @@ let make = (~urlEntityName, ~baseUrlForRedirection, ~connectorVariant) => {
| _ => "By default, payments are routed in the order shown here i.e. top to bottom. To change the priority, just drag and drop the processors to reorder them."
}

let connectorPath = switch connectorVariant {
| ConnectorTypes.PayoutProcessor => "/payoutconnectors"
| _ => "/connectors"
}

let settingUpConnectorsState = routingRespArray => {
let profileList =
routingRespArray->Array.filter(value =>
value->getDictFromJsonObject->getString("profile_id", "") === profileId
value->getDictFromJsonObject->getString("profile_id", "") === profile
)

let connectorList = switch profileList->Array.get(0) {
Expand Down Expand Up @@ -71,7 +79,7 @@ let make = (~urlEntityName, ~baseUrlForRedirection, ~connectorVariant) => {
settingUpConnectorsState(defaultRoutingResponse)
}
None
}, [profileId])
}, [profile])

let handleChangeOrder = async () => {
try {
Expand All @@ -81,7 +89,7 @@ let make = (~urlEntityName, ~baseUrlForRedirection, ~connectorVariant) => {
let defaultFallbackUpdateUrl = `${getURL(
~entityName=urlEntityName,
~methodType=Post,
)}/profile/${profileId}`
)}/profile/${profile}`

(
await updateDetails(defaultFallbackUpdateUrl, defaultPayload->JSON.Encode.array, Post)
Expand Down Expand Up @@ -109,8 +117,24 @@ let make = (~urlEntityName, ~baseUrlForRedirection, ~connectorVariant) => {

<PageLoaderWrapper
screenState
customUI={<NoDataFound message="Please connect at least 1 connector" renderType=Painting />}>
customUI={<div className="mt-2 mb-6">
<RoutingHelper.NoProcessorFound
connectorPath subtitle="Please connect at least 1 connector to manage this configuration."
/>
</div>}>
<div className="flex flex-col gap-6 my-6">
<Form initialValues={Dict.make()->JSON.Encode.object}>
<div className="w-full md:w-1/2 lg:w-1/3">
<BasicDetailsForm.BusinessProfileInp
setProfile
profile
options={MerchantAccountUtils.businessProfileNameDropDownOption(
businessProfileRecoilVal,
)}
label="Profile"
/>
</div>
</Form>
<AlertV2Binding
alertType=Primary
slot={{slot: <Icon name="nd-info-circle" size=20 className="text-nd_primary_blue-500" />}}
Expand Down
25 changes: 25 additions & 0 deletions src/screens/Routing/RoutingHelper.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
open Typography

module NoProcessorFound = {
@react.component
let make = (
~connectorPath,
~subtitle="Please connect at least 1 processor in order to create a rule.",
) => {
<div
className="flex flex-col items-center justify-center gap-6 min-h-80-vh w-full border border-nd_gray-150 rounded-lg">
<div className="flex flex-col items-center gap-2">
<p className={`${heading.sm.semibold} text-nd_gray-800`}>
{React.string("No Processor Found")}
</p>
<p className={`${body.md.medium} text-nd_gray-500`}> {React.string(subtitle)} </p>
</div>
<Button
text="Connect Processor"
buttonType=Primary
buttonSize=Medium
onClick={_ => RescriptReactRouter.push(GlobalVars.appendDashboardPath(~url=connectorPath))}
/>
</div>
}
}
Loading
Loading