Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move around some stuff in core._ #2313

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion phoenix-scala/core/app/core/db/Star.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ object * extends LazyLogging {
DbResultT.fromResult(fa)

def <~[A](v: A)(implicit ec: EC): DbResultT[A] =
DbResultT.pure(v)
v.pure[DbResultT]

def <~[A](v: Validated[Failures, A])(implicit ec: EC): DbResultT[A] =
DbResultT.fromEither(v.toEither)
Expand Down
23 changes: 7 additions & 16 deletions phoenix-scala/core/app/core/db/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,14 @@ package object db {
}

trait FoxyTFunctions[F[_]] {
def apply[A](a: A)(implicit F: Monad[F]): FoxyT[F, A] = // TODO: remove me? @michalrus
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dead code.

pure(a)

def pure[A](a: A)(implicit F: Monad[F]): FoxyT[F, A] =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not needed, we have pure from cats.

Monad[FoxyT[F, ?]].pure(a)

def good[A](a: A)(implicit F: Monad[F]): FoxyT[F, A] = // TODO: remove me @michalrus
pure(a)
a.pure[FoxyT[F, ?]]

def unit(implicit F: Monad[F]): FoxyT[F, Unit] =
pure(()) // TODO: remove me? @michalrus
().pure[FoxyT[F, ?]] // TODO: remove me? @michalrus

def none[A](implicit F: Monad[F]): FoxyT[F, Option[A]] =
pure(None) // TODO: remove me? @michalrus
(None: Option[A]).pure[FoxyT[F, ?]] // TODO: remove me? @michalrus

def uiWarning(f: Failure)(implicit F: Monad[F]): FoxyT[F, Unit] =
StateT.modify(MetaResponse.Warning(f) :: _)
Expand Down Expand Up @@ -161,7 +155,7 @@ package object db {
L.map(lfa)(_.fold(Either.left(_), Either.right(_))).sequence.flatMap { xa ⇒
val failures = L.collect(xa) { case Left(f) ⇒ f.toList }.toList.flatten
val values = L.collect(xa) { case Right(a) ⇒ a }
NonEmptyList.fromList(failures).fold(FoxyT[F].pure(values))(FoxyT[F].failures(_))
NonEmptyList.fromList(failures).fold(values.pure[FoxyT[F, ?]])(FoxyT[F].failures(_))
}

/** A bit like ``sequence`` but will ignore failed Foxies. */
Expand Down Expand Up @@ -276,15 +270,12 @@ package object db {
def appendForUpdate[A, B <: slick.dbio.NoStream](sql: SqlAction[A, B, Effect.Read]): DBIO[A] =
sql.overrideStatements(sql.statements.map(_ + " for update"))

def lift[A](value: A): DBIO[A] = DBIO.successful(value)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not needed, we have pure from cats.


def liftFuture[A](future: Future[A]): DBIO[A] = DBIO.from(future)

// TODO: I don’t know… does this help? @michalrus
def ifElse[A](condition: Boolean, ifBranch: ⇒ DbResultT[A], elseBranch: ⇒ DbResultT[A]) =
if (condition) ifBranch else elseBranch

def doOrMeh(condition: Boolean, action: ⇒ DbResultT[_])(implicit ec: EC): DbResultT[Unit] =
if (condition) action.meh else DbResultT.unit
def when[F[_]](p: Boolean, s: ⇒ F[Unit])(implicit F: Applicative[F]): F[Unit] =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

if (p) s.void else F.pure(())

def doOrGood[A](condition: Boolean, action: ⇒ DbResultT[A], good: ⇒ A)(implicit ec: EC): DbResultT[A] =
if (condition) action else DbResultT.good(good)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ object ObjectUtils {
for {
commit ← * <~ ObjectCommits.create(ObjectCommit(formId = form.id, shadowId = shadow.id))
} yield commit.some
else DbResultT.pure(None)
else none[ObjectCommit].pure[DbResultT]

private def updateIfDifferent(
old: FormAndShadow,
Expand All @@ -244,14 +244,14 @@ object ObjectUtils {
}
_ ← * <~ validateShadow(form, shadow)
} yield UpdateResult(form, shadow, updated = true)
else DbResultT.pure(UpdateResult(old.form, old.shadow, updated = false))
else UpdateResult(old.form, old.shadow, updated = false).pure[DbResultT]

private def validateShadow(form: ObjectForm, shadow: ObjectShadow)(implicit ec: EC,
fmt: Formats): DbResultT[Unit] =
failIfErrors(IlluminateAlgorithm.validateAttributes(form.attributes, shadow.attributes))

def failIfErrors(errors: Seq[Failure])(implicit ec: EC): DbResultT[Unit] = errors match {
case head :: tail ⇒ DbResultT.failures(NonEmptyList(head, tail))
case Nil ⇒ DbResultT.pure(Unit)
case Nil ⇒ ().pure[DbResultT]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package objectframework.models

import java.time.Instant

import cats.implicits._
import core.db.ExPostgresDriver.api._
import core.db._
import objectframework.services.ObjectManager
Expand Down Expand Up @@ -88,7 +89,7 @@ object ObjectHeadLinks {
def createIfNotExist(left: L, right: R)(implicit ec: EC, db: DB): DbResultT[Unit] =
for {
linkExists ← * <~ filterLeft(left).filter(_.rightId === right.id).exists.result
_ ← * <~ doOrMeh(!linkExists, create(build(left, right)))
_ ← * <~ when(!linkExists, create(build(left, right)).void)
} yield {}

def build(left: L, right: R): M
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package objectframework.services

import org.json4s._

import cats.data._
import cats.implicits._

import cats.kernel.Monoid
import core.db.ExPostgresDriver.api._
import core.db._
import core.failures.Failure

import objectframework.IlluminateAlgorithm
import objectframework.content._
import objectframework.db._
Expand Down Expand Up @@ -54,7 +52,7 @@ object ContentManager {

type FullContentRelations = Map[String, Seq[Content]]
def getRelations(content: Content)(implicit ec: EC): DbResultT[FullContentRelations] = {
val empty = DbResultT.pure(Map.empty[String, Seq[Content]])
val empty = (Map.empty : FullContentRelations).pure[DbResultT]

content.relations.foldLeft(empty) { (accRelations, relation) ⇒
accRelations.flatMap { relations ⇒
Expand Down Expand Up @@ -106,6 +104,6 @@ object ContentManager {
private def failIfErrors(errors: Seq[Failure])(implicit ec: EC): DbResultT[Unit] =
errors match {
case head :: tail ⇒ DbResultT.failures(NonEmptyList(head, tail))
case Nil ⇒ DbResultT.pure(Unit)
case Nil ⇒ ().pure[DbResultT]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ case class CustomerGroup(id: Int = 0,
scope: LTree,
createdBy: Int,
name: String,
customersCount: Int = 0,
customersCount: Int = 0, // FIXME: is this denormalization needed at all? https://foxcommerce.slack.com/archives/C06696D1R/p1498564090580988 @michalrus
clientState: Json,
elasticRequest: Json,
groupType: GroupType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ object SearchReference {
def productsSearchField: String = "productId"
def skuSearchField: String = "code"

def pureMetrics(implicit ec: EC): Result[Long] = Result.pure(0L)
def pureBuckets(implicit ec: EC): Result[Buckets] = Result.pure(Seq.empty)
def pureMetrics(implicit ec: EC): Result[Long] = 0L.pure[Result]
def pureBuckets(implicit ec: EC): Result[Buckets] = (Seq.empty : Buckets).pure[Result]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package phoenix.responses

import java.time.Instant

import cats.implicits._
import core.db._
import core.utils.Money._
import phoenix.models.account.{Organization, Organizations, Users}
import phoenix.models.account.{Organization, Organizations, User, Users}
import phoenix.models.admin.AdminsData
import phoenix.models.customer.CustomersData
import phoenix.models.payment.giftcard.GiftCard
Expand Down Expand Up @@ -103,9 +104,9 @@ object ReturnResponse {
// Either customer or storeAdmin as creator
customer ← * <~ Users.findOneByAccountId(rma.accountId)
customerData ← * <~ CustomersData.findOneByAccountId(rma.accountId)
storeAdmin ← * <~ rma.storeAdminId.map(Users.findOneByAccountId).getOrElse(lift(None))
adminData ← * <~ rma.storeAdminId.map(AdminsData.findOneByAccountId).getOrElse(lift(None))
organization ← * <~ rma.storeAdminId.map(Organizations.mustFindByAccountId)
storeAdmin rma.storeAdminId.flatTraverse(Users.findOneByAccountId(_).dbresult)
adminData ← * <~ rma.storeAdminId.flatTraverse(AdminsData.findOneByAccountId(_).dbresult)
organization ← * <~ rma.storeAdminId.traverse(Organizations.mustFindByAccountId)
// Payment methods
ccPayment ← * <~ ReturnPayments.findAllByReturnId(rma.id).creditCards.one
applePayPayment ← * <~ ReturnPayments.findAllByReturnId(rma.id).applePays.one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object CordResponsePromotions {
/** Try `fa` and if it’s `None`, evaluate and fallback to `fb`. Basically, `Option#orElse` lifted to `DbResultT`. */
private def lazyOrElse[A](fa: DbResultT[Option[A]], fb: ⇒ DbResultT[Option[A]])(
implicit ec: EC): DbResultT[Option[A]] =
fa.flatMap(_.map(a ⇒ DbResultT.pure(a.some)).getOrElse(fb))
fa.flatMap(_.map(a ⇒ a.some.pure[DbResultT]).getOrElse(fb))

private def renderPromotionResponse(
promotion: Promotion)(implicit ec: EC, ctx: OC, db: DB): DbResultT[PromotionResponse.Root] =
Expand Down
2 changes: 1 addition & 1 deletion phoenix-scala/phoenix/app/phoenix/routes/AuthRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ object AuthRoutes {

pathPrefix("public") {
(post & path("login") & entity(as[LoginPayload])) { payload ⇒
doLogin(DbResultT.pure(payload))(_.runDBIO())
doLogin(payload.pure[DbResultT] )(_.runDBIO())
} ~
activityContext(defaultScope) { implicit ac ⇒
(post & path("send-password-reset") & pathEnd & entity(as[ResetPasswordSend])) { payload ⇒
Expand Down
18 changes: 10 additions & 8 deletions phoenix-scala/phoenix/app/phoenix/services/Capture.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ case class Capture(payload: CapturePayloads.Capture)(implicit ec: EC, db: DB, ap
externalCaptureTotal ← * <~ determineExternalCapture(total, gcPayments, scPayments, order.currency)
internalCaptureTotal = total - externalCaptureTotal
_ ← * <~ internalCapture(internalCaptureTotal, order, customer, gcPayments, scPayments)
_ ← * <~ doOrMeh(externalCaptureTotal > 0, externalCapture(externalCaptureTotal, order))
_ ← * <~ when(externalCaptureTotal > 0, externalCapture(externalCaptureTotal, order))

resp = CaptureResponse(
order = order.refNum,
Expand Down Expand Up @@ -142,8 +142,8 @@ case class Capture(payload: CapturePayloads.Capture)(implicit ec: EC, db: DB, ap
scIds = scPayments.map { case (_, sc) ⇒ sc.id }.distinct
gcCodes = gcPayments.map { case (_, gc) ⇒ gc.code }.distinct

_ ← * <~ doOrMeh(scTotal > 0, LogActivity().scFundsCaptured(customer, order, scIds, scTotal))
_ ← * <~ doOrMeh(gcTotal > 0, LogActivity().gcFundsCaptured(customer, order, gcCodes, gcTotal))
_ ← * <~ when(scTotal > 0, LogActivity().scFundsCaptured(customer, order, scIds, scTotal).void)
_ ← * <~ when(gcTotal > 0, LogActivity().gcFundsCaptured(customer, order, gcCodes, gcTotal).void)
} yield {}

private def externalCapture(total: Long, order: Order): DbResultT[Unit] = {
Expand Down Expand Up @@ -277,10 +277,12 @@ case class Capture(payload: CapturePayloads.Capture)(implicit ec: EC, db: DB, ap
private def getPrice(item: OrderLineItemProductData): DbResultT[LineItemPrice] =
FormShadowGet.price(item.skuForm, item.skuShadow) match {
case Some((price, currency)) ⇒
DbResultT.pure(LineItemPrice(item.lineItem.referenceNumber, item.sku.code, price, currency))
LineItemPrice(item.lineItem.referenceNumber, item.sku.code, price, currency).pure[DbResultT]
case None ⇒ DbResultT.failure(CaptureFailures.SkuMissingPrice(item.sku.code))
}

// FIXME: use MonadError below, no need for DbResultT @michalrus

private def validatePayload(payload: CapturePayloads.Capture,
orderSkus: Seq[OrderLineItemProductData]): DbResultT[Unit] =
for {
Expand All @@ -299,12 +301,12 @@ case class Capture(payload: CapturePayloads.Capture)(implicit ec: EC, db: DB, ap
private def paymentStateMustBeInAuth(order: Order, paymentState: CordPaymentState.State): DbResultT[Unit] =
if (paymentState != CordPaymentState.Auth)
DbResultT.failure(CaptureFailures.OrderMustBeInAuthState(order.refNum))
else DbResultT.pure(Unit)
else ().pure[DbResultT]

private def mustHavePositiveShippingCost(shippingCost: CapturePayloads.ShippingCost): DbResultT[Unit] =
if (shippingCost.total < 0)
DbResultT.failure(CaptureFailures.ShippingCostNegative(shippingCost.total))
else DbResultT.pure(Unit)
else ().pure[DbResultT]

private def mustHaveCodes(items: Seq[CapturePayloads.CaptureLineItem],
codes: Seq[String],
Expand All @@ -316,10 +318,10 @@ case class Capture(payload: CapturePayloads.Capture)(implicit ec: EC, db: DB, ap
private def mustHaveCode(item: CapturePayloads.CaptureLineItem,
codes: Seq[String],
orderRef: String): DbResultT[Unit] =
if (codes.contains(item.sku)) DbResultT.pure(Unit)
if (codes.contains(item.sku)) ().pure[DbResultT]
else DbResultT.failure(CaptureFailures.SkuNotFoundInOrder(item.sku, orderRef))

private def mustHaveSameLineItems(lOne: Int, lTwo: Int, orderRef: String): DbResultT[Unit] =
if (lOne == lTwo) DbResultT.pure(Unit)
if (lOne == lTwo) ().pure[DbResultT]
else DbResultT.failure(CaptureFailures.SplitCaptureNotSupported(orderRef))
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ case class CartValidator(cart: Cart)(implicit ec: EC, db: DB, ctx: OC) extends C
// Deactivation/archival don’t happen often enough to justify
// this additional overhead for each cart GET request.
hasActiveLineItems(state)
} else DbResultT.pure(state))
} else state.pure[DbResultT])
state ← * <~ hasShipAddress(state)
state ← * <~ validShipMethod(state)
state ← * <~ sufficientPayments(state, isCheckout)
Expand Down Expand Up @@ -145,7 +145,7 @@ case class CartValidator(cart: Cart)(implicit ec: EC, db: DB, ctx: OC) extends C
// we'll find out if the `ExternalFunds` doesn't auth at checkout but we presume sufficient funds if we have a
// `ExternalFunds` regardless of GC/SC funds availability
if (payments.exists(_.isExternalFunds)) {
lift(response)
response.pure[DBIO]
} else if (payments.nonEmpty) {
cartFunds(payments).map {
case Some(funds) if funds >= grandTotal ⇒
Expand All @@ -155,7 +155,7 @@ case class CartValidator(cart: Cart)(implicit ec: EC, db: DB, ctx: OC) extends C
warning(response, InsufficientFunds(cart.refNum))
}
} else {
lift(warning(response, InsufficientFunds(cart.refNum)))
warning(response, InsufficientFunds(cart.refNum)).pure[DBIO]
}

if (cart.grandTotal > 0 || cart.subTotal > 0) {
Expand All @@ -164,7 +164,7 @@ case class CartValidator(cart: Cart)(implicit ec: EC, db: DB, ctx: OC) extends C
.result
.flatMap(availableFunds(cart.grandTotal, _))
} else {
lift(response)
response.pure[DBIO]
}
}

Expand Down
15 changes: 8 additions & 7 deletions phoenix-scala/phoenix/app/phoenix/services/Checkout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ object PaymentHelper {
apis: Apis,
ac: AC): DbResultT[Long] =
if (payments.isEmpty) {
DbResultT.pure(0L)
0L.pure[DbResultT]
} else {

val amounts: Seq[Long] = payments.map { case (payment, _) ⇒ payment.getAmount() }
Expand Down Expand Up @@ -195,6 +195,7 @@ object Checkout {
}

class ExternalCalls {
// FIXME: have mercy… @michalrus
@volatile var authPaymentsSuccess: Boolean = false
@volatile var middleWarehouseSuccess: Boolean = false
}
Expand Down Expand Up @@ -240,10 +241,10 @@ case class Checkout(
liSkus ← * <~ CartLineItems.byCordRef(cart.refNum).countSkus
inventoryTrackedSkus ← * <~ filterInventoryTrackingSkus(liSkus)
skusToHold ← * <~ inventoryTrackedSkus.map(sku ⇒ SkuInventoryHold(sku.code, sku.qty))
_ ← * <~ doOrMeh(skusToHold.nonEmpty,
_ ← * <~ when(skusToHold.nonEmpty,
DbResultT.fromResult(
apis.middlewarehouse.hold(OrderInventoryHold(cart.referenceNumber, skusToHold))))
mutating = externalCalls.middleWarehouseSuccess = skusToHold.nonEmpty
mutating = externalCalls.middleWarehouseSuccess = skusToHold.nonEmpty // FIXME: I almost removed that, having read `==` here. Please, don’t… @michalrus
} yield {}

private def filterInventoryTrackingSkus(skus: Map[String, Int]) =
Expand Down Expand Up @@ -331,13 +332,13 @@ case class Checkout(
scIds = scPayments.map { case (_, sc) ⇒ sc.id }.distinct
gcCodes = gcPayments.map { case (_, gc) ⇒ gc.code }.distinct

_ ← * <~ doOrMeh(scTotal > 0, LogActivity().scFundsAuthorized(customer, cart, scIds, scTotal))
_ ← * <~ doOrMeh(gcTotal > 0, LogActivity().gcFundsAuthorized(customer, cart, gcCodes, gcTotal))
_ ← * <~ when(scTotal > 0, LogActivity().scFundsAuthorized(customer, cart, scIds, scTotal).void)
_ ← * <~ when(gcTotal > 0, LogActivity().gcFundsAuthorized(customer, cart, gcCodes, gcTotal).void)

grandTotal = cart.grandTotal
internalPayments = gcTotal + scTotal
_ ← * <~ doOrMeh(grandTotal > internalPayments, // run external payments only if we have to pay more
doExternalPayment(grandTotal - internalPayments))
_ ← * <~ when(grandTotal > internalPayments, // run external payments only if we have to pay more
doExternalPayment(grandTotal - internalPayments).void)

mutatingResult = externalCalls.authPaymentsSuccess = true // fixme is this flag used anywhere? @aafa
} yield {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ object CreditCardManager {
.map(_.gatewayCustomerId)
.one
address = Address.fromPayload(payload.billingAddress, customer.accountId)
_ ← * <~ doOrMeh(payload.addressIsNew, Addresses.create(address))
_ ← * <~ when(payload.addressIsNew, Addresses.create(address).void)
stripes ← * <~ apis.stripe.createCardFromToken(email = customer.email,
token = payload.token,
stripeCustomerId = customerToken,
Expand All @@ -69,7 +69,7 @@ object CreditCardManager {

def createCard(customer: User, sCustomer: StripeCustomer, sCard: StripeCard, address: Address) =
for {
_ ← * <~ doOrMeh(address.isNew, Addresses.create(address.copy(accountId = accountId)))
_ ← * <~ when(address.isNew, Addresses.create(address.copy(accountId = accountId)).void)
cc = CreditCard.buildFromSource(accountId, sCustomer, sCard, payload, address)
newCard ← * <~ CreditCards.create(cc)
region ← * <~ Regions.findOneById(newCard.address.regionId).safeGet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ object AccountManager {
.filter(_.state === (AdminData.Invited: AdminData.State))
.one
_ ← * <~ adminCreated.map(ad ⇒ AdminsData.update(ad, ad.copy(state = AdminData.Active)))
_ ← * <~ doOrMeh(adminCreated.isEmpty, LogActivity().userPasswordReset(user))
_ ← * <~ when(adminCreated.isEmpty, LogActivity().userPasswordReset(user).void)
} yield ResetPasswordDoneAnswer(email = remind.email, org = organization.name)

def getById(accountId: Int)(implicit ec: EC, db: DB): DbResultT[UserResponse] =
Expand All @@ -114,7 +114,7 @@ object AccountManager {
.findByNameInScope(context.org, scope.id)
.mustFindOr(OrganizationNotFound(context.org, scope.path))

_ ← * <~ doOrMeh(checkEmail, email.fold(DbResultT.unit)(Users.createEmailMustBeUnique))
_ ← * <~ when(checkEmail, email.fold(DbResultT.unit)(Users.createEmailMustBeUnique))

account ← * <~ Accounts.create(Account())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RemorseTimer(implicit db: DB, ec: EC, apis: Apis) extends Actor {
val query = for {
cordRefs ← * <~ orders.result
count ← * <~ orders.map(_.state).update(newState)
_ ← * <~ doOrMeh(count > 0, logAcitvity(newState, cordRefs))
_ ← * <~ when(count > 0, logAcitvity(newState, cordRefs))
} yield count

RemorseTimerResponse(query.runTxn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ object CartLineItemUpdater {
.filterByContext(ctx.id)
.filter(_.code === lineItem.sku)
.mustFindOneOr(SkuNotFoundForContext(lineItem.sku, ctx.id))
fullSku ← ObjectManager.getFullObject(DbResultT.pure(sku))
fullSku ← ObjectManager.getFullObject(sku.pure[DbResultT])
_ ← * <~ IlluminatedSku.illuminate(ctx, fullSku).mustBeActive
// TODO: check if that SKU’s Product is not archived/deactivated @michalrus
_ ← * <~ mustFindProductIdForSku(sku, cart.refNum)
Expand Down
Loading