Created
October 9, 2012 05:19
-
-
Save victorstanciu/3856766 to your computer and use it in GitHub Desktop.
mobilpay request
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class Auction extends Record | |
{ | |
const STATUS_AUTO_DRAFT = 'auto-draft'; | |
const STATUS_DRAFT = 'draft'; | |
const STATUS_ACTIVE = 'active'; | |
const STATUS_RESERVED = 'reserved'; | |
const STATUS_CANCELED = 'canceled'; | |
const TYPE_AUCTION = 0; | |
const TYPE_STATIC = 1; | |
const TYPE_PROMOTION = 2; | |
const TYPE_USER_LISTING = 3; | |
protected $_live; | |
public function getExpiryRequest() | |
{ | |
$request = new Mobilpay_Payment_Request_Sms(); | |
$config = Nip_Config::instance()->MOBILPAY; | |
$request->url = $config->sandbox ? $config->sandbox_request_url : $config->request_url; | |
$request->signature = $config->merchant_id; | |
$request->service = $config->listing_service_id; | |
$request->returnUrl = Listings::instance()->getMineURL(); | |
$request->confirmUrl = Listings::instance()->getMobilpayURL(); | |
$request->orderId = $this->id . md5(uniqid()); | |
$request->encrypt(LIBRARY_PATH . "Mobilpay" . DS . "public.cer"); | |
return $request; | |
} | |
public function getTopListingRequest() | |
{ | |
$request = new Mobilpay_Payment_Request_Sms(); | |
$config = Nip_Config::instance()->MOBILPAY; | |
$request->url = $config->sandbox ? $config->sandbox_request_url : $config->request_url; | |
$request->signature = $config->merchant_id; | |
$request->service = $config->top_listing_service_id; | |
$request->returnUrl = Listings::instance()->getMineURL(); | |
$request->confirmUrl = Listings::instance()->getMobilpayURL(); | |
$request->orderId = $this->id . md5(uniqid()); | |
$request->encrypt(LIBRARY_PATH . "Mobilpay" . DS . "public.cer"); | |
return $request; | |
} | |
public function expired() | |
{ | |
return strtotime($this->expires) < time(); | |
} | |
/** | |
* @return Listing | |
*/ | |
public function toUserListing() | |
{ | |
$item = Listings::instance()->getNew(); | |
$item->writeData($this->toArray()); | |
return $item; | |
} | |
public function getStatusString() | |
{ | |
$strings = array( | |
self::STATUS_AUTO_DRAFT => 'Auto draft', | |
self::STATUS_DRAFT => 'Inactiv', | |
self::STATUS_ACTIVE => 'Activ', | |
self::STATUS_RESERVED => 'Rezervat', | |
self::STATUS_CANCELED => 'Anulat' | |
); | |
return $strings[$this->status]; | |
} | |
/** | |
* Returns the next live auction in the queue, only if the next item is | |
* distinct than the current one. If no auction for the same car model is | |
* found, a random one is returned | |
* | |
* @return Auction | |
*/ | |
public function getNextLiveAuction() | |
{ | |
$manager = Auctions::instance(); | |
$query = $manager->getCurrentQuery(); | |
$filters = array( | |
"new" => $this->new, | |
"id_manufacturer" => $this->getManufacturer()->id | |
); | |
$manager->filter($query, $filters); | |
$item = $manager->findOneByQuery($query); | |
if (!$item) { | |
unset($filters['id_manufacturer']); | |
$item = $manager->getRandomCurrent($filters); | |
} | |
return $item; | |
} | |
public function process(&$queue, $offer = false) | |
{ | |
$this->price -= $this->getPriceDecrement(); | |
if ($this->price < $this->min_price) { | |
$this->start($queue); | |
return; | |
} | |
if ($offer && $offer->price >= $this->price) { | |
return $this->reserve($offer); | |
} | |
$this->save(array("price")); | |
return $this; | |
} | |
public function start(&$queue = array()) | |
{ | |
$rand = rand(1, 2); | |
$this->min_price = $this->{"min_price_$rand"}; | |
$this->price = $this->initial_price; | |
$this->status = self::STATUS_ACTIVE; | |
$this->id_user_reserved = 0; | |
$this->reserved = null; | |
$this->iterations++; | |
$this->queue($queue); | |
$this->save(array('min_price', 'price', 'status', 'id_user_reserved', 'reserved', 'iterations', 'queue')); | |
// send previous offers to dealer, then delete | |
$offers = $this->getOffers(); | |
if (count($offers)) { | |
$this->getDealer()->sendOffers($offers); | |
$offers->delete(); | |
} | |
return $this; | |
} | |
/** | |
* @param array $queue | |
* @return bool | |
*/ | |
public function queue(&$queue = array()) | |
{ | |
$this->queue = ++$queue[$this->new][$this->id_manufacturer]; | |
return $this; | |
} | |
/** | |
* @param Offer|User $item | |
* @return Auction | |
*/ | |
public function reserve($item) | |
{ | |
if ($item instanceof Offer) { | |
$offer = $item; | |
$user = $item->getUser(); | |
} else { | |
$user = $item; | |
} | |
$this->id_user_reserved = $user->id; | |
$this->status = self::STATUS_RESERVED; | |
$this->reserved = date(DATE_DB); | |
$this->save(array('id_user_reserved', 'status', 'reserved')); | |
$this->sendNotifications(); | |
// delete all auction and user offers, except the one that reserved the auction | |
$offers = $this->getOffers(); | |
unset($offers[$offer->id]); | |
$offers->delete(); | |
$offers = $user->getOffers(); | |
unset($offers[$offer->id]); | |
$offers->delete(); | |
return $this; | |
} | |
public function sendAlerts() | |
{ | |
$query = Alerts::instance()->newQuery(); | |
$query->where("new != ?", abs(1 - $this->new)); | |
$query->where("id_model = ?", $this->id_model); | |
$alerts = Alerts::instance()->findByQuery($query); | |
if (count($alerts)) { | |
foreach ($alerts as $alert) { | |
if ($alert->match($this)) { | |
$alert->send($this); | |
} | |
} | |
} | |
return $this; | |
} | |
public function sendNotifications() | |
{ | |
$user = $this->getReservedUser(); | |
$dealer = $this->getDealer(); | |
$view = new Nip_View(); | |
$view->setBasePath(MODULES_PATH . 'default/views/'); | |
$view->user = $user; | |
$view->dealer = $dealer; | |
$view->auction = $this; | |
foreach (array("user", "dealer") as $who) { | |
$view->setBlock('content', '/email/auction_reserved_' . $who); | |
${$who . "_content"} = $view->load('/layouts/email', array(), true); | |
$processor = new CSSToInlineStyles(${$who . "_content"}, file_get_contents(STYLESHEETS_PATH . 'email.css')); | |
${$who . "_content"} = $processor->convert(); | |
} | |
$email = new Nip_View_Email(); | |
$email->setSubject(__("Felicitari!")); | |
$email->addTo($user->email, $user->name); | |
$email->setBody($user_content); | |
$email->send(); | |
$email->clearAllRecipients(); | |
$email->setSubject(__("Notificare")); | |
$email->addTo($dealer->email, $dealer->name); | |
foreach (explode(",", Nip_Config::instance()->EMAIL->offers) as $address) { | |
$email->addBCC($address); | |
} | |
$email->setBody($dealer_content); | |
$email->send(); | |
return $this; | |
} | |
/** | |
* @return ModelFuel | |
*/ | |
public function getModelFuel() | |
{ | |
return ModelFuels::instance()->findOne($this->id_model_fuel); | |
} | |
/** | |
* @return ModelBody | |
*/ | |
public function getModelBody() | |
{ | |
return ModelBodies::instance()->findOne($this->id_model_body); | |
} | |
/** | |
* @return Model | |
*/ | |
public function getModel() | |
{ | |
return Models::instance()->findOne($this->id_model); | |
} | |
/** | |
* @return Manufacturer | |
*/ | |
public function getManufacturer() | |
{ | |
$manager = Manufacturers::instance(); | |
if ($this->id_manufacturer) { | |
$return = $manager->findOne($this->id_manufacturer); | |
} | |
if (!$return) { | |
$model = $this->getModel(); | |
if ($model) { | |
$this->id_manufacturer = $model->id_manufacturer; | |
$this->save(array('id_manufacturer')); | |
$return = $model->getManufacturer(); | |
} | |
} | |
return $return; | |
} | |
public function getName() | |
{ | |
$parts[] = $this->getModel()->getName(); | |
$parts[] = $this->model_variant; | |
return implode(" ", array_map("trim", $parts)); | |
} | |
public function getID() | |
{ | |
return str_pad($this->id, 3, '0', STR_PAD_LEFT); | |
} | |
/** | |
* @return Dealer | |
*/ | |
public function getDealer() | |
{ | |
return Dealers::instance()->findOne($this->id_dealer); | |
} | |
/** | |
* @return County | |
*/ | |
public function getCounty() | |
{ | |
return Counties::instance()->findOne($this->id_county); | |
} | |
/** | |
* Returns the auction's creator | |
* @return User | |
*/ | |
public function getUser() | |
{ | |
return Users::instance()->findOne($this->id_user); | |
} | |
/** | |
* Returns the user (if any) that has successfully reserved this auction, | |
* either via direct reservation or through the offer system | |
*/ | |
public function getReservedUser() | |
{ | |
return Users::instance()->findOne($this->id_user_reserved); | |
} | |
/** | |
* @todo fetch value from the database | |
* @return int | |
*/ | |
public function getPriceDecrement() | |
{ | |
return $this->new() ? 100 : 50; | |
} | |
public function getPrice() | |
{ | |
return $this->price; | |
} | |
public function active() | |
{ | |
return $this->status == self::STATUS_ACTIVE; | |
} | |
public function reserved() | |
{ | |
return $this->status == self::STATUS_RESERVED; | |
} | |
public function canceled() | |
{ | |
return $this->status == self::STATUS_CANCELED; | |
} | |
public function isStatic() | |
{ | |
return intval($this->static) === self::TYPE_STATIC; | |
} | |
public function isAuction() | |
{ | |
return intval($this->static) === self::TYPE_AUCTION; | |
} | |
public function isPromotion() | |
{ | |
return intval($this->static) === self::TYPE_PROMOTION; | |
} | |
public function isUserListing() | |
{ | |
return intval($this->static) === self::TYPE_USER_LISTING; | |
} | |
public function live() | |
{ | |
if (is_null($this->_live)) { | |
$current = $this->getManager()->getCurrent(); | |
if ($current[$this->id]) { | |
$this->_live = true; | |
} else { | |
$this->_live = false; | |
} | |
} | |
return $this->_live; | |
} | |
public function activate() | |
{ | |
$this->status = self::STATUS_ACTIVE; | |
$this->save(); | |
return $this; | |
} | |
public function deactivate($notify_user = false) | |
{ | |
$this->status = self::STATUS_DRAFT; | |
$this->queue = $this->price = $this->min_price = 0; | |
$this->save(); | |
$this->getOffers()->delete(); | |
if ($notify_user) { | |
$this->sendExpiryNotification(); | |
} | |
return $this; | |
} | |
public function sendExpiryNotification() | |
{ | |
$user = $this->getUser(); | |
$view = new Nip_View(); | |
$view->setBasePath(MODULES_PATH . 'default/views/'); | |
$view->setBlock('content', '/email/auction_expired'); | |
$view->auction = $this; | |
$view->user = $user; | |
$content = $view->load('/layouts/email', array(), true); | |
$processor = new CSSToInlineStyles($content, file_get_contents(STYLESHEETS_PATH . 'email.css')); | |
$content = $processor->convert(); | |
$email = new Nip_View_Email(); | |
$email->setSubject(__('Autobis.ro - Anuntul tau a expirat')); | |
$email->addTo($this->getUser()->email, $this->getUser()->name); | |
$email->setBody($content); | |
$email->send(); | |
} | |
public function validateAdmin($request = array()) | |
{ | |
if ($request) { | |
$this->getFromRequest($request, array( | |
"id_manufacturer", "id_model", "id_county", "new", | |
"static", "initial_price", "min_price_1", "rabla", | |
"model_variant", "model_doors", "id_model_fuel", "model_price", "model_engine", "model_power", "model_gearbox", "model_mileage", "model_emissions", "model_co2", "model_color", "model_year", "model_km", "model_warranty", "model_description", "model_tax", | |
"real_images", | |
"promotion_discount", "promotion_available", "promotion_optionals_price", "promotion_financing", "contact_email", "contact_telephone" | |
)); | |
if ($this->getUser()) { | |
$fields = array_merge($fields, array('billing', 'tos', 'contact_name', 'contact_email', 'contact_telephone')); | |
} | |
if (isset($request['id_dealer'])) { | |
$this->id_dealer = intval($request['id_dealer']); | |
} | |
} | |
$this->validateNew(); | |
$this->validateModel(); | |
$this->validateSpecifications(); | |
$this->validatePrice(); | |
if ($this->isPromotion()) { | |
$this->validatePromotion(); | |
} | |
if ($this->active()) { | |
$this->validateImages(); | |
} | |
$this->validateOptions($request); | |
$this->validateFeatures($request); | |
return !count($this->errors); | |
} | |
public function validate($request = array(), $validate_billing = false) | |
{ | |
if ($this->isUserListing()) { | |
return $this->toUserListing()->validate($request); | |
} | |
if ($request) { | |
$fields = array( | |
"id_manufacturer", "id_model", "id_county", "new", | |
"static", "initial_price", "min_price_1", "rabla", | |
"model_variant", "model_doors", "id_model_fuel", "model_price", "model_engine", "model_power", "model_gearbox", "model_mileage", "model_emissions", "model_co2", "model_color", "model_year", "model_km", "model_warranty", "model_description", "model_tax", | |
"real_images" | |
); | |
if ($this->getUser()) { | |
$fields = array_merge($fields, array('billing', 'tos', 'contact_name', 'contact_email', 'contact_telephone')); | |
} | |
$this->getFromRequest($request, $fields); | |
if (isset($request['id_dealer'])) { | |
$this->id_dealer = intval($request['id_dealer']); | |
} | |
} | |
$this->validateNew(); | |
$this->validateModel(); | |
$this->validateSpecifications(); | |
$this->validateImages(); | |
$this->validateOptions($request); | |
$this->validateFeatures($request); | |
$this->validatePrice(); | |
if ($this->getUser()) { | |
$this->validateContact(); | |
} | |
if ($validate_billing) { | |
$this->validateTos(); | |
} | |
return !count($this->errors); | |
} | |
public function validateNew() | |
{ | |
if (!strlen($this->new)) { | |
$this->errors['new'] = __('Nu ai ales tipul anuntului'); | |
} | |
} | |
public function validateModel() | |
{ | |
if (!$this->getModel()) { | |
$this->errors['id_model'] = __('Nu ai ales modelul'); | |
} | |
} | |
public function validatePrice() | |
{ | |
if (!($this->initial_price > 0)) { | |
if ($this->static()) { | |
$this->errors['initial_price'] = __('Nu ai introdus pretul'); | |
} else { | |
$this->errors['initial_price'] = __('Nu ai introdus pretul de start'); | |
} | |
} | |
if ($this->static()) { | |
$this->min_price_1 = $this->min_price_2 = 0; | |
} else { | |
if (!($this->min_price_1 > 0)) { | |
$this->errors['min_price_1'] = __('Nu ai introdus pretul minim'); | |
return false; | |
} | |
$config = Nip_Config::instance()->AUCTIONS; | |
$discount = $this->new() ? $config->discount_new : $config->discount_old; | |
if ($this->initial_price - $discount < $this->min_price_1) { | |
$this->errors['min_price'] = __('Diferenta intre pretul de start si pretul minim trebuie sa fie de cel putin #{discount} euro', array( | |
'discount' => $discount | |
)); | |
return false; | |
} | |
$this->min_price_2 = $this->min_price_1 + ($this->new() ? 200 : 100); | |
} | |
} | |
public function validateSpecifications() | |
{ | |
if (!strlen($this->model_variant) || $this->model_variant == __('Versiune')) { | |
$this->errors['model_variant'] = __('Nu ai completat versiunea'); | |
} | |
if (!strlen($this->model_engine) || $this->model_engine == __('Motor') || $this->model_engine == __('Capacitate cilindrica')) { | |
$this->errors['model_engine'] = __('Nu ai completat motorul / capacitatea cilindrica'); | |
} | |
if (!intval($this->model_doors)) { | |
$this->model_doors = 0; | |
} | |
if (!strlen($this->model_power) || $this->model_power == __('Putere maxima (CP)')) { | |
$this->errors['model_power'] = __('Nu ai completat puterea maxima'); | |
} | |
if (!strlen($this->model_gearbox) || $this->model_gearbox == __('Cutie de viteze')) { | |
$this->errors['model_gearbox'] = __('Nu ai completat cutia de viteze'); | |
} | |
if (!$this->getModelFuel()) { | |
$this->errors['id_model_fuel'] = __('Nu ai ales combustibilul'); | |
} | |
if (!strlen($this->model_emissions) || $this->model_emissions == __('Alege norma de poluare')) { | |
$this->errors['model_emissions'] = __('Nu ai ales norma de poluare'); | |
} | |
if (!in_array($this->model_year, range(date("Y"), date("Y") - 10))) { | |
$this->errors['model_year'] = __('Nu ai selectat anul de fabricatie'); | |
} | |
if ($this->model_co2 == __('Emisii CO2 (g/km)')) { | |
$this->model_co2 = ''; | |
} | |
if ($this->model_mileage == __('Consum Mixt')) { | |
$this->model_mileage = ''; | |
} | |
if (!strlen($this->model_color) || $this->model_color == __('Culoare')) { | |
$this->errors['model_color'] = __('Nu ai completat culoarea'); | |
} | |
if ($this->model_tax == __('Taxa auto') || $this->model_tax == __('Taxa de poluare')) { | |
$this->model_tax = ''; | |
} | |
if (!$this->getCounty()) { | |
$this->errors['id_county'] = __('Nu ai ales judetul'); | |
} | |
if ($this->new()) { | |
if (!($this->model_price > 0)) { | |
$this->errors['model_price'] = __('Nu ai completat pretul de lista'); | |
} | |
if (!strlen(trim($this->model_description)) || $this->model_description == __('Optiuni incluse in pret')) { | |
$this->errors['model_description'] = __('Nu ai completat optiunile incluse in pret'); | |
} | |
} else { | |
if (!strlen(trim($this->model_description)) || $this->model_description == __('Descriere completa')) { | |
$this->errors['model_description'] = __('Nu ai completat descrierea completa'); | |
} | |
} | |
if (!strlen($this->model_warranty) || $this->model_warranty == __('Garantie')) { | |
$this->errors['model_warranty'] = __('Nu ai completat garantia'); | |
} | |
} | |
public function validatePromotion() | |
{ | |
if (!($this->promotion_discount > 0)) { | |
$this->errors['promotion_discount'] = __('Discount-ul Autobis trebuie sa fie mai mare decat zero'); | |
} | |
if (!strlen($this->promotion_financing)) { | |
$this->errors['promotion_financing'] = __('Nu ai completat informatiile legate de finantare'); | |
} | |
if (!($this->promotion_available > 0)) { | |
$this->errors['promotion_available'] = __('Nu ai introdus numarul de unitati disponibile'); | |
} | |
if (!strlen($this->contact_email)) { | |
$this->errors['contact_email'] = __('Nu ai introdus adresa de email de contact'); | |
} elseif (!valid_email($this->contact_email)) { | |
$this->errors['contact_email'] = __('Adresa de email de contact nu este valida'); | |
} | |
if (!strlen($this->contact_telephone)) { | |
$this->errors['contact_telephone'] = __('Nu ai introdus numarul de telefon de contact'); | |
} | |
} | |
public function validateImages() | |
{ | |
if (!count($this->getImages())) { | |
$this->errors['images'] = __('Trebuie sa incarci cel putin o imagine'); | |
} | |
if ($this->real_images === '') { | |
$this->real_images = 1; | |
} | |
} | |
public function validateOptions($request = array()) | |
{ | |
$ids = $request['options'] ? array_map('intval', $request['options']) : array(); | |
$options = ListingOptions::instance()->findAll(); | |
foreach ($options as $option) { | |
if (in_array($option->id, $ids)) { | |
$this->getOptions()->add($option); | |
} else { | |
$this->getOptions()->remove($option); | |
} | |
} | |
} | |
public function validateFeatures($request) | |
{ | |
$ids = $request['features'] ? array_map('intval', $request['features']) : array(); | |
$features = ListingFeatures::instance()->findAll(); | |
foreach ($features as $feature) { | |
if (in_array($feature->id, $ids)) { | |
$this->getFeatures()->add($feature); | |
} else { | |
$this->getFeatures()->remove($feature); | |
} | |
} | |
} | |
public function validateTos() | |
{ | |
if (!$this->tos) { | |
$this->errors['tos'] = __('Trebuie sa accepti Termenii si Conditiile de utilizare'); | |
} | |
} | |
public function validateContact() | |
{ | |
if (!strlen($this->contact_name) || $this->contact_name == __('Nume complet')) { | |
$this->errors['contact_name'] = __('Nu ai completat numele persoanei de contact'); | |
} | |
if (!strlen($this->contact_email) || $this->contact_email == __('Adresa de email')) { | |
$this->errors['contact_email'] = __('Nu ai completat adresa de email persoanei de contact'); | |
} elseif (!valid_email($this->contact_email)) { | |
$this->errors['contact_email'] = __('Adresa de email a persoanei de contact nu este valida'); | |
} | |
if (!strlen($this->contact_telephone) || $this->contact_telephone == __('Telefon')) { | |
$this->errors['contact_telephone'] = __('Nu ai completat numarul de telefon al persoanei de contact'); | |
} | |
if (!$this->getCounty()) { | |
$this->errors['id_county'] = __('Nu ai selectat judetul'); | |
} | |
} | |
/** | |
* @return AuctionImage | |
*/ | |
public function getNewImage() | |
{ | |
$image = AuctionImages::instance()->getNew(); | |
$image->id_auction = $this->id; | |
$image->position = 1; | |
$images = $this->getOrderedImages(); | |
if (count($images)) { | |
$image->position = $images->end()->position + 1; | |
} | |
return $image; | |
} | |
/** | |
* @return AuctionImage | |
*/ | |
public function getImage() | |
{ | |
$images = $this->getOrderedImages(); | |
if (count($images)) { | |
return $images->rewind(); | |
} | |
return $this->getNewImage(); | |
} | |
public function orderImages($order = array()) | |
{ | |
if (!count($order)) { | |
return; | |
} | |
$images = $this->getImages(); | |
foreach ($order as $id) { | |
$images[$id]->position = ++$i; | |
$images[$id]->save(); | |
} | |
return $this; | |
} | |
/** | |
* To be used as a callback for usort-type functions | |
*/ | |
public function sortImages($a, $b) | |
{ | |
if ($a->position == $b->position) { | |
return 0; | |
} | |
return ($a->position < $b->position) ? -1 : 1; | |
} | |
public function getOrderedImages() | |
{ | |
$images = $this->getImages(); | |
$images->uasort(array($this, "sortImages")); | |
return $images; | |
} | |
public function uploadImage($data = array()) | |
{ | |
$upload = new Nip_File_Upload($data); | |
if (!$upload->valid(Nip_File_Upload::VALIDATE_IMAGE)) { | |
return $upload->getError(); | |
} | |
$image = $this->getNewImage(); | |
$image->upload($upload); | |
return $image; | |
} | |
public function cancel() | |
{ | |
$this->status = self::STATUS_CANCELED; | |
$this->save(); | |
return $this; | |
} | |
public function delete() | |
{ | |
$images = $this->getImages(); | |
if (count($images)) { | |
foreach ($images as $image) { | |
$image->delete(); | |
} | |
} | |
$this->getOffers()->delete(); | |
Nip_File_System::instance()->removeDirectory($this->getUploadPath()); | |
return parent::delete(); | |
} | |
public function getUploadPath() | |
{ | |
return UPLOADS_PATH . 'images' . DS . 'auctions' . DS . $this->id; | |
} | |
public function getUploadURL() | |
{ | |
return UPLOADS_URL . "images/auctions/{$this->id}"; | |
} | |
public function getURL() | |
{ | |
if (Nip_Request::instance()->module == 'admin') { | |
$arguments = func_get_args(); | |
return parent::__call('getURL', $arguments); | |
} | |
$params = array( | |
"new" => $this->isPromotion() ? 'promotii-autobis' : ($this->new() ? "anunturi-masini-noi" : "anunturi-masini-rulate"), | |
"name" => Nip_Helper_URL::instance()->encode($this->getName()), | |
"id" => $this->id | |
); | |
return $this->URL()->Auctions()->view($params); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment