Skip to content

Commit c1d52d6

Browse files
authored
Merge branch 'main' into ac/pm-32394-scim-v2-feature-flag-impl
2 parents c4f9740 + e758ca2 commit c1d52d6

File tree

4 files changed

+492
-4
lines changed

4 files changed

+492
-4
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,13 @@ jobs:
209209
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
210210
SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only
211211
IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag
212-
IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags
213212
fi
214213
215214
if [[ "$IMAGE_TAG" == "main" ]]; then
216215
IMAGE_TAG=dev
217216
fi
218-
217+
218+
IMAGE_TAG=${IMAGE_TAG:0:128} # Limit image tags to 128 chars
219219
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
220220
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> "$GITHUB_STEP_SUMMARY"
221221

src/Core/Billing/Services/Implementations/StripePaymentService.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,8 @@ public async Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber)
661661
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(discount);
662662
}
663663

664+
await ApplySchedulePhase2DataAsync(subscription, subscriptionInfo);
665+
664666
var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(subscription);
665667

666668
if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)
@@ -702,6 +704,74 @@ public async Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber)
702704
return subscriptionInfo;
703705
}
704706

707+
private async Task ApplySchedulePhase2DataAsync(Subscription subscription, SubscriptionInfo subscriptionInfo)
708+
{
709+
if (string.IsNullOrEmpty(subscription.ScheduleId))
710+
{
711+
return;
712+
}
713+
714+
try
715+
{
716+
var schedule = await _stripeAdapter.GetSubscriptionScheduleAsync(subscription.ScheduleId,
717+
new SubscriptionScheduleGetOptions
718+
{
719+
Expand = ["phases.discounts.coupon", "phases.items.price"]
720+
});
721+
722+
if (schedule.Status != StripeConstants.SubscriptionScheduleStatus.Active || schedule.Phases.Count < 2)
723+
{
724+
return;
725+
}
726+
727+
var phase2 = schedule.Phases[1];
728+
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
729+
730+
if (phase2.StartDate < now)
731+
{
732+
return;
733+
}
734+
735+
// Override line item amounts with Phase 2 prices
736+
if (phase2.Items != null && subscriptionInfo.Subscription?.Items != null)
737+
{
738+
var items = subscriptionInfo.Subscription.Items.ToList();
739+
foreach (var phase2Item in phase2.Items)
740+
{
741+
if (phase2Item.Price is not { UnitAmount: not null, ProductId: not null })
742+
{
743+
continue;
744+
}
745+
746+
var matchingItem = items.FirstOrDefault(i => i.ProductId == phase2Item.Price.ProductId);
747+
if (matchingItem != null)
748+
{
749+
matchingItem.Amount = phase2Item.Price.UnitAmount.Value / 100M;
750+
}
751+
}
752+
753+
subscriptionInfo.Subscription.Items = items;
754+
}
755+
756+
// Override discount with Phase 2 discount
757+
var phase2Discount = phase2.Discounts?.FirstOrDefault();
758+
if (phase2Discount?.Coupon != null)
759+
{
760+
subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(phase2Discount.Coupon);
761+
}
762+
}
763+
catch (StripeException ex)
764+
{
765+
// Fallback: user sees current Phase 1 prices instead of Phase 2 prices.
766+
// Accepted tradeoff: showing current data is better than failing the page.
767+
_logger.LogWarning(ex,
768+
"Failed to retrieve subscription schedule ({ScheduleId}) for subscription ({SubscriptionId}), Phase 2 data resolution. Error Code: {ErrorCode}",
769+
subscription.ScheduleId,
770+
subscription.Id,
771+
ex.StripeError?.Code);
772+
}
773+
}
774+
705775
public async Task<string> AddSecretsManagerToSubscription(
706776
Organization org,
707777
StaticStore.Plan plan,

src/Core/Models/Business/SubscriptionInfo.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ public BillingCustomerDiscount(Discount discount)
5252
AppliesTo = discount.Coupon?.AppliesTo?.Products;
5353
}
5454

55+
/// <summary>
56+
/// Creates a BillingCustomerDiscount from a Stripe Coupon object.
57+
/// Unlike <see cref="BillingCustomerDiscount(Discount)"/>, this constructor does not have
58+
/// access to a Discount wrapper, so Active is unconditionally set to true.
59+
/// </summary>
60+
public BillingCustomerDiscount(Coupon coupon)
61+
{
62+
Id = coupon.Id;
63+
Active = true;
64+
PercentOff = coupon.PercentOff;
65+
AmountOff = ConvertFromStripeMinorUnits(coupon.AmountOff);
66+
AppliesTo = coupon.AppliesTo?.Products;
67+
}
68+
5569
/// <summary>
5670
/// The Stripe coupon ID (e.g., "cm3nHfO1").
5771
/// Note: Only specific coupon IDs are displayed in the UI based on feature flag configuration,
@@ -60,8 +74,8 @@ public BillingCustomerDiscount(Discount discount)
6074
public string? Id { get; set; }
6175

6276
/// <summary>
63-
/// True only for perpetual/recurring discounts (End == null).
64-
/// False for any discount with an expiration date, even if not yet expired.
77+
/// When constructed from a Discount, true only for perpetual discounts (End == null).
78+
/// When constructed from a Coupon directly, always true (no end-date information available).
6579
/// Product decision for Milestone 2: only show perpetual discounts in UI.
6680
/// </summary>
6781
public bool Active { get; set; }

0 commit comments

Comments
 (0)