Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ public function ingestExternalTicketData($summit_id)
path: '/api/v1/summits/{id}/tickets/csv/template',
operationId: 'getTicketImportTemplate',
summary: 'Get ticket import template',
description: 'Returns a CSV template for importing ticket data',
description: 'Returns a CSV template for importing ticket data. Includes one column per summit badge feature name plus one extra_question:{question name} column per summit order extra question (Ticket/Both usage).',
security: [['summit_tickets_oauth2' => [
SummitScopes::WriteSummitData,
SummitScopes::WriteRegistrationData,
Expand Down Expand Up @@ -788,7 +788,8 @@ public function getImportTicketDataTemplate($summit_id)
* ticket_promo_code (optional)
* badge_type_id (optional)
* badge_type_name (optional)
* badge_features (optional)
* badge_features (optional, one col per badge feature name)
* extra_question:{question name} (optional, one col per order extra question - Ticket/Both usage)
*/

$summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id);
Expand Down Expand Up @@ -816,6 +817,11 @@ public function getImportTicketDataTemplate($summit_id)
$row[$featuresType->getName()] = '';
}

// order extra questions for summit ( ticket / attendee scoped ones )
foreach ($summit->getOrderExtraQuestionsByUsage(SummitOrderExtraQuestionTypeConstants::TicketQuestionUsage) as $question) {
$row[sprintf('%s%s', ISummitOrderService::ExtraQuestionColumnPrefix, $question->getName())] = '';
}

$template = [
$row
];
Expand All @@ -835,7 +841,7 @@ public function getImportTicketDataTemplate($summit_id)
path: '/api/v1/summits/{id}/tickets/csv',
operationId: 'importTicketData',
summary: 'Import ticket data from CSV',
description: 'Imports ticket data from a CSV file',
description: 'Imports ticket data from a CSV file. Supported columns: id, number, attendee_email, attendee_first_name, attendee_last_name, attendee_tags, attendee_company, attendee_company_id, ticket_type_name, ticket_type_id, promo_code_id, promo_code, ticket_promo_code, badge_type_id, badge_type_name, one column per badge feature name (1/0) and one extra_question:{question name} column per order extra question (Ticket/Both usage; for list type questions use the value name/label, "|" separated for multi value).',
security: [['summit_tickets_oauth2' => [
SummitScopes::WriteSummitData,
SummitScopes::WriteRegistrationData,
Expand Down
3 changes: 3 additions & 0 deletions app/Services/Model/ISummitOrderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
*/
interface ISummitOrderService extends IProcessPaymentService
{
// ticket data import csv column naming convention for order extra question answers
const ExtraQuestionColumnPrefix = 'extra_question:';

/**
* @param Member|null $owner
* @param Summit $summit
Expand Down
184 changes: 184 additions & 0 deletions app/Services/Model/Imp/SummitOrderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use App\Jobs\ProcessTicketDataImport;
use App\Jobs\SendAttendeeInvitationEmail;
use App\Jobs\Utils\JobDispatcher;
use App\Models\Foundation\ExtraQuestions\ExtraQuestionType;
use App\Models\Foundation\ExtraQuestions\ExtraQuestionTypeConstants;
use App\Models\Foundation\Summit\Factories\SummitOrderFactory;
use App\Models\Foundation\Summit\Registration\IBuildDefaultPaymentGatewayProfileStrategy;
use App\Models\Foundation\Summit\Registration\PromoCodes\PromoCodesUtils;
Expand Down Expand Up @@ -4284,6 +4286,7 @@ public function importTicketData(Summit $summit, UploadedFile $csv_file): void
* badge_type_id (optional)
* badge_type_name (optional)
* one col per feature
* extra_question:{question name} (optional, one col per order extra question - Ticket/Both usage)
*/

// validate format with col names
Expand Down Expand Up @@ -4583,6 +4586,11 @@ public function processTicketData(int $summit_id, string $filename)

Log::debug(sprintf("SummitOrderService::processTicketData - got ticket %s (%s)", $ticket->getId(), $ticket->getNumber()));

// extra questions ( extra_question:{question name} columns )
$answers_owner = !is_null($attendee) ? $attendee : ($ticket->hasOwner() ? $ticket->getOwner() : null);
if (!is_null($answers_owner))
$this->upsertAttendeeExtraQuestionAnswers($summit, $answers_owner, $row);

// badge data
if (!$badge_data_present) {
Log::warning("SummitOrderService::processTicketData badge data is not present stop current row processing.");
Expand Down Expand Up @@ -4657,6 +4665,182 @@ public function processTicketData(int $summit_id, string $filename)
$this->download_strategy->delete($path);
}

/**
* Upserts attendee extra question answers from a ticket data import row
* (one column per question, "extra_question:{question name}" naming convention).
* Unknown question names, order-scoped questions, disallowed questions and empty values are
* skipped, never fail the row. Mandatory-question completeness is not enforced ( admin bulk load ).
* Former answers not present on the row are carried over on a best effort basis: the shared
* persistence path ( ExtraQuestionAnswerHolder::hadCompletedExtraQuestions ) rebuilds the full
* answers set and drops answers whose question no longer exists or is no longer allowed for the
* attendee, same as the admin attendee update flow.
* @param Summit $summit
* @param SummitAttendee $attendee
* @param array $row
* @throws ValidationException
*/
private function upsertAttendeeExtraQuestionAnswers(Summit $summit, SummitAttendee $attendee, array $row): void
{
$new_answers = [];

foreach ($row as $col => $value) {

if (!str_starts_with(strval($col), self::ExtraQuestionColumnPrefix)) continue;

$question_name = trim(substr($col, strlen(self::ExtraQuestionColumnPrefix)));
$value = trim(strval($value));

if ($value === '') {
Log::debug
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers question %s has an empty value for attendee %s, skipping it",
$question_name,
$attendee->getEmail()
)
);
continue;
}

$question = $summit->getOrderExtraQuestionByName($question_name);
if (is_null($question)) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers question %s does not exist on summit %s, skipping it",
$question_name,
$summit->getId()
)
);
continue;
}

if ($question->getUsage() === SummitOrderExtraQuestionTypeConstants::OrderQuestionUsage) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers question %s is order scoped, can not be answered per attendee, skipping it",
$question_name
)
);
continue;
}

if (!$attendee->isAllowedQuestion($question)) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers question %s is not allowed for attendee %s, skipping it",
$question_name,
$attendee->getEmail()
)
);
continue;
}

if ($question->allowsValues()) {
// list type questions store the value id(s) ( comma separated when multi value )
// csv cells accept the value name/label ( "|" separated for multi value ) or the raw value id
$value_ids = [];
foreach (explode('|', $value) as $v) {
$v = trim($v);
if ($v === '') continue;
$question_value = $question->getValueByName($v);
if (is_null($question_value))
$question_value = $question->getValueByLabel($v);
if (is_null($question_value) && is_numeric($v))
$question_value = $question->getValueById(intval($v));
if (is_null($question_value)) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers value %s does not exist on question %s, skipping it",
$v,
$question_name
)
);
continue;
}
$value_ids[] = $question_value->getId();
}
if (count($value_ids) === 0) continue;
// only CheckBoxList questions admit multiple selected values
if (count($value_ids) > 1 && $question->getType() !== ExtraQuestionTypeConstants::CheckBoxListQuestionType) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers question %s admits a single value, got %s, skipping it",
$question_name,
count($value_ids)
)
);
continue;
}
$value = implode(ExtraQuestionType::QuestionChoicesCharSeparator, $value_ids);
}

$former_answer = $attendee->getExtraQuestionAnswerByQuestion($question);
$former_value = is_null($former_answer) ? '' : $former_answer->getValue();
if (!empty($former_value) && $former_value != $value && !$attendee->canChangeAnswerValue($question)) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers answer for question %s can not be changed by this time for attendee %s, skipping it",
$question_name,
$attendee->getEmail()
)
);
continue;
}

$new_answers[$question->getId()] = $value;
}

if (count($new_answers) === 0) return;

if (!$attendee->hasAllowedExtraQuestions()) {
Log::warning
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers attendee %s does not have allowed extra questions, skipping answers",
$attendee->getEmail()
)
);
return;
}

// upsert semantics: the persistence path rebuilds the full answers set, so carry over
// the former answers not overridden by the csv row ( best effort, see docblock )
$extra_questions = [];
foreach ($attendee->getExtraQuestionAnswers() as $former_answer) {
$question_id = $former_answer->getQuestionId();
if (!array_key_exists($question_id, $new_answers))
$extra_questions[] = ['question_id' => $question_id, 'answer' => $former_answer->getValue()];
}
foreach ($new_answers as $question_id => $value)
$extra_questions[] = ['question_id' => $question_id, 'answer' => $value];

Log::debug
(
sprintf
(
"SummitOrderService::upsertAttendeeExtraQuestionAnswers attendee %s answers %s",
$attendee->getEmail(),
json_encode($extra_questions)
)
);

$attendee->hadCompletedExtraQuestions($extra_questions);
}

/**
* @param Summit $summit
* @param array $payload
Expand Down
Loading
Loading