private[productmove] def subscriptionCancel()

in handlers/product-move-api/src/main/scala/com/gu/productmove/endpoint/cancel/SubscriptionCancelEndpointSteps.scala [38:126]


  private[productmove] def subscriptionCancel(
      subscriptionName: SubscriptionName,
      postData: ExpectedInput,
      identityId: IdentityId,
  ): Task[OutputBody] = {
    val maybeResult: IO[OutputBody | Throwable, Success] = for {
      subscriptionAccount <- assertSubscriptionBelongsToIdentityUser(
        getSubscription,
        getAccount,
        subscriptionName,
        Some(identityId),
      )
      _ <- ZIO.log(s"Cancel Supporter Plus - PostData: $postData")
      subscription <- getSubscriptionToCancel.get(subscriptionName)
      _ <- ZIO.log(s"Subscription is $subscription")

      _ <- subscription.status match {
        case "Active" => ZIO.succeed(())
        case _ => ZIO.fail(BadRequest(s"Subscription $subscriptionName cannot be cancelled as it is not active"))
      }

      // check sub info to make sure it's a supporter plus subscription
      zuoraIds <- ZIO
        .fromEither(ZuoraIds.zuoraIdsForStage(config.Stage(stage.toString)).left.map(InternalServerError.apply))
      // We have fetched the subscription with the charge-detail=current-segment query param described here:
      // https://developer.zuora.com/api-references/api/operation/GET_SubscriptionsByKey/#!in=query&path=charge-detail&t=request
      // this means that only the currently active rate plan will contain charge information (even if it has a
      // lastChangeType of 'Remove')
      ratePlan <- asSingle(
        subscription.ratePlans.filter { ratePlan =>
          val isDiscount = ratePlan.productName == "Discounts"
          // we found that contrary to the docs, if effectiveStartDate and effectiveEndDate are today,
          // zuora still returns it as a current-segment, so we need to ignore it
          val hasCharges = ratePlan.ratePlanCharges.exists(_.effectiveEndDate != today)
          !isDiscount && hasCharges
        },
        "ratePlan",
      )
      charges <- asNonEmptyList(
        ratePlan.ratePlanCharges,
        s"Subscription can't be cancelled as the charge list is empty",
      )
      supporterPlusCharge <- getSupporterPlusCharge(charges, zuoraIds.supporterPlusZuoraIds)

      today <- Clock.currentDateTime.map(_.toLocalDate)

      // check whether the sub is within the first 14 days of purchase - if it is then the subscriber is entitled to a refund
      shouldBeRefunded = subIsWithinFirst14Days(today, supporterPlusCharge.effectiveStartDate)
      _ <- ZIO.log(s"Should be refunded is $shouldBeRefunded")

      cancellationDate <- ZIO
        .fromOption(
          if (shouldBeRefunded)
            Some(supporterPlusCharge.effectiveStartDate)
          else
            supporterPlusCharge.chargedThroughDate,
        )
        .orElseFail(
          InternalServerError(
            s"Subscription charged through date is null for supporter plus subscription ${subscriptionName.value}. " +
              s"This is an error because we expect to be able to use the charged through date to work out the effective cancellation date",
          ),
        )
      _ <- ZIO.log(s"Cancellation date is $cancellationDate")

      _ <- ZIO.log(s"Attempting to cancel sub")
      cancellationResponse <- zuoraCancel.cancel(subscriptionName, cancellationDate)
      _ <- ZIO.log("Sub cancelled as of: " + cancellationDate)

      _ <-
        if (shouldBeRefunded)
          doRefund(subscriptionName, cancellationResponse)
        else
          ZIO.succeed(RefundResponse("Success", ""))

      _ <- ZIO.log(s"Attempting to update cancellation reason on Zuora subscription")
      _ <- zuoraSetCancellationReason
        .update(
          subscriptionName,
          subscription.version + 1,
          postData.reason,
        ) // Version +1 because the cancellation will have incremented the version
      _ <- sqs.sendEmail(EmailMessage.cancellationEmail(subscriptionAccount._2, cancellationDate))
    } yield Success(s"Subscription ${subscriptionName.value} was successfully cancelled")
    maybeResult.catchAll {
      case failure: OutputBody => ZIO.succeed(failure)
      case other: Throwable => ZIO.fail(other)
    }
  }