Skip to content

feat(ACL-306): Add support for Transaction Verified Payouts#254

Merged
tl-Roberto-Mancinelli merged 1 commit intomainfrom
ACL-306
Oct 17, 2025
Merged

feat(ACL-306): Add support for Transaction Verified Payouts#254
tl-Roberto-Mancinelli merged 1 commit intomainfrom
ACL-306

Conversation

@tl-Roberto-Mancinelli
Copy link
Collaborator

@tl-Roberto-Mancinelli tl-Roberto-Mancinelli commented Oct 15, 2025

Summary

Adds support for Verified Payouts to the TrueLayer .NET client library, enabling verification of payout beneficiaries before funds are transferred. This is a UK-only feature that allows merchants to verify account holder names and optionally search for specific transactions before executing payouts.

Implements the API specification from: https://docs.truelayer.com/docs/make-a-verified-payout

Changes

New Models

Verification Models (/src/TrueLayer/Payouts/Model/Verification.cs)

  • Verification: Configuration for verified payouts
    • VerifyName: Boolean flag for name verification
    • TransactionSearchCriteria: Optional transaction search configuration
  • TransactionSearchCriteria: Criteria for transaction detection
    • Tokens: Search tokens to find in transaction descriptions
    • AmountInMinor: Expected transaction amount
    • Currency: Transaction currency
    • CreatedAt: Expected transaction date (serialized as YYYY-MM-DD)

User Models (/src/TrueLayer/Payouts/Model/PayoutUser.cs)

  • PayoutUserRequest: User details for CREATE payout requests
    • Id, Name, Email, Phone, DateOfBirth, Address (all optional)
  • PayoutUserResponse: User details in API responses
    • Id: User identifier

Beneficiary Models

  • Updated CreatePayoutBeneficiary.UserDetermined: Beneficiary type for verified payouts

    • Reference: Payout reference for bank statement
    • User: PayoutUserRequest with beneficiary details
    • Verification: Verification configuration
    • ProviderSelection: Provider selection (UserSelected or Preselected)
    • Note: Does NOT include account details (user provides during verification flow)
  • Updated CreatePayoutBeneficiary.BusinessAccount: Simplified to match API spec

    • Removed AccountHolderName and AccountIdentifier parameters
    • Only requires Reference
  • New GetPayoutBeneficiary namespace: Separate beneficiary types for GET responses

    • PaymentSource: Added AccountHolderName and AccountIdentifiers fields
    • UserDetermined: Includes optional AccountHolderName, AccountIdentifiers, and Verification
    • ExternalAccount, BusinessAccount: Match GET response formats

Response Models (/src/TrueLayer/Payouts/Model/CreatePayoutResponse.cs)

  • CreatePayoutResponse.AuthorizationRequired: Response for verified payouts requiring authorization
    • Id: Payout identifier
    • Status: Always "authorization_required"
    • ResourceToken: Token for HPP authentication
    • User: User details with ID
  • CreatePayoutResponse.Created: Response for standard non-verified payouts
    • Id: Payout identifier
    • Marked with [DefaultJsonDiscriminator] for fallback deserialization

HPP Link Builder (/src/TrueLayer/Payouts/Model/PayoutHppLinkBuilder.cs)

Static helper class for building Hosted Payment Page verification links:

  • CreateVerificationLink(string payoutId, string resourceToken, string returnUri, bool useSandbox): Base method
  • CreateVerificationLink(AuthorizationRequired response, string returnUri, bool useSandbox): Convenience overload

Returns properly formatted HPP URLs:

  • Sandbox: https://app.truelayer-sandbox.com/payouts#payout_id={id}&resource_token={token}&return_uri={encoded_uri}
  • Production: https://app.truelayer.com/payouts#payout_id={id}&resource_token={token}&return_uri={encoded_uri}

Enhanced JSON Serialization

Default Discriminator Support (/src/TrueLayer/Serialization/DefaultJsonDiscriminatorAttribute.cs)

  • New [DefaultJsonDiscriminator] attribute for fallback type matching
  • Enhanced OneOfJsonConverter to support default types when no discriminator field is present
  • Enables handling of responses like {"id": "..."} without a status field

API Updates

Updated Return Types

  • IPayoutsApi.CreatePayout: Returns OneOf<AuthorizationRequired, Created>
  • IPayoutsApi.GetPayout: Updated to use GetPayoutBeneficiary types
  • GetTransactions: Updated to use GetPayoutBeneficiary types for payout transactions

Tests

Unit Tests (/test/TrueLayer.Tests/Payouts/PayoutsApiTests.cs)

  • CreatePayout_With_Verified_Payout_Returns_AuthorizationRequired: Verifies verified payout creation
  • CreateVerificationLink_Should_Build_Correct_Sandbox_Url: Tests sandbox HPP link generation
  • CreateVerificationLink_Should_Build_Correct_Production_Url: Tests production HPP link generation
  • CreateVerificationLink_From_Response_Should_Build_Correct_Url: Tests convenience overload
  • CreateVerificationLink_With_Empty_ResourceToken_Should_Throw: Tests validation
  • CreateVerificationLink_With_Real_Production_Data_Should_Build_Valid_Link: Tests realistic production data
  • CreateVerificationLink_With_Real_Sandbox_Data_Should_Build_Valid_Link: Tests realistic sandbox data
  • CreateVerificationLink_With_Complex_Return_Uri_Should_Encode_Properly: Tests URL encoding

Acceptance Tests (/test/TrueLayer.AcceptanceTests/PayoutTests.cs)

  • Can_create_verified_payout_with_name_verification: Tests name-only verification
  • Can_create_verified_payout_with_transaction_verification: Tests name + transaction verification
  • Can_create_verified_payout_with_preselected_provider: Tests with preselected provider

Based on sandbox test requirements from: https://docs.truelayer.com/docs/make-a-verified-payout#test-verified-payouts-in-sandbox

Breaking Changes

⚠️ Type Changes:

  • CreatePayout now returns OneOf<AuthorizationRequired, Created> instead of CreatePayoutResponse
  • Consumers must use .Match() to handle both response types
  • Beneficiary renamed to CreatePayoutBeneficiary for clarity
  • Added separate GetPayoutBeneficiary types for GET responses

Migration Example:

// Before
var response = await client.Payouts.CreatePayout(request);
var payoutId = response.Data.Id;

// After
var response = await client.Payouts.CreatePayout(request);
var payoutId = response.Data.Match(
    authRequired => authRequired.Id,
    created => created.Id);

// For verified payouts, handle authorization flow
response.Data.Match(
    authRequired => 
    {
        var hppLink = PayoutHppLinkBuilder.CreateVerificationLink(
            authRequired, 
            "https://myapp.com/callback", 
            useSandbox: true);
        // Redirect user to hppLink for verification
    },
    created => 
    {
        // Standard payout created, no verification needed
    });

Testing

All tests pass successfully:

  • ✅ 15 unit tests (across .NET 6.0, 8.0, 9.0)
  • ✅ 3 new acceptance tests for verified payouts
  • ✅ Full build with no errors

Documentation

Added comprehensive XML documentation for all new types and methods, including:

  • Detailed parameter descriptions
  • Usage examples
  • Links to TrueLayer API documentation
  • Sandbox testing instructions in test code comments

Checklist

  • Implementation matches TrueLayer API specification
  • All existing tests pass
  • New tests added for verified payouts functionality
  • HPP link builder tested with realistic data
  • URL encoding tested with complex return URIs
  • Separate CREATE and GET beneficiary types
  • XML documentation added
  • Breaking changes documented
  • Build succeeds with no errors

@dili91 dili91 requested a review from Copilot October 16, 2025 07:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds support for Verified Payouts to the TrueLayer .NET client library, enabling UK merchants to verify payout beneficiaries before funds are transferred. The feature allows verification of account holder names and optionally searching for specific transactions before executing payouts.

Key changes include:

  • New verification models and beneficiary types for user-determined payouts
  • Updated return types to handle both standard and verified payout responses
  • HPP link builder helper for generating verification URLs

Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/TrueLayer/Payouts/Model/Verification.cs New verification models for name and transaction verification
src/TrueLayer/Payouts/Model/PayoutUser.cs New user models for beneficiary details in verified payouts
src/TrueLayer/Payouts/Model/CreatePayoutBeneficiary.cs Updated beneficiary types with UserDetermined support
src/TrueLayer/Payouts/Model/CreatePayoutResponse.cs New response types for AuthorizationRequired and Created responses
src/TrueLayer/Payouts/Model/PayoutHppLinkBuilder.cs Helper class for building verification HPP links
src/TrueLayer/Payouts/IPayoutsApi.cs Updated CreatePayout return type to OneOf union
test/TrueLayer.Tests/Payouts/PayoutsApiTests.cs Comprehensive unit tests for verified payouts and HPP link generation
test/TrueLayer.AcceptanceTests/PayoutTests.cs Acceptance tests for verified payout scenarios

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@tl-Roberto-Mancinelli tl-Roberto-Mancinelli marked this pull request as ready for review October 16, 2025 07:58
@tl-Roberto-Mancinelli tl-Roberto-Mancinelli requested review from a team as code owners October 16, 2025 07:58
{
const string Discriminator = "user_determined";

public UserDetermined(string reference, string accountHolderName, AccountIdentifierUnion accountIdentifier)
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we already support UD?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes it was already there

{
const string Discriminator = "user_determined";

public UserDetermined(string reference, string accountHolderName, AccountIdentifierUnion accountIdentifier)

This comment was marked as duplicate.

/// <param name="address">The physical address of the end user</param>
public PayoutUserRequest(
string? id = null,
string? name = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Name must be provided for TVP, at least until we only support regulated clients.
With that said, LGTM - It makes sense to be flexible.

/// <summary>
/// Helper class for building verification Hosted Payment Page (HPP) links for verified payouts
/// </summary>
public static class PayoutHppLinkBuilder
Copy link
Contributor

Choose a reason for hiding this comment

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

It's HP2, not HPP in theory... With that said I'm happy keeping a simple unified name, I would not bother about this detail here

dili91
dili91 previously approved these changes Oct 17, 2025
Copy link
Contributor

@dili91 dili91 left a comment

Choose a reason for hiding this comment

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

🎖️

add changelog

fix typo

add AuthorizationRequired in GetPayoutsResponse

update changelog
@tl-Roberto-Mancinelli tl-Roberto-Mancinelli merged commit 2190414 into main Oct 17, 2025
1 check passed
@tl-Roberto-Mancinelli tl-Roberto-Mancinelli deleted the ACL-306 branch October 17, 2025 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants