|
| 1 | +import builtins |
1 | 2 | import uuid |
2 | 3 | from collections.abc import Sequence |
3 | 4 | from datetime import UTC, datetime |
|
14 | 15 | from polar.config import Environment, settings |
15 | 16 | from polar.customer.repository import CustomerRepository |
16 | 17 | from polar.enums import InvoiceNumbering |
17 | | -from polar.exceptions import NotPermitted, PolarError, PolarRequestValidationError |
| 18 | +from polar.exceptions import ( |
| 19 | + NotPermitted, |
| 20 | + PolarError, |
| 21 | + PolarRequestValidationError, |
| 22 | + ValidationError, |
| 23 | +) |
18 | 24 | from polar.integrations.loops.service import loops as loops_service |
19 | 25 | from polar.integrations.plain.service import plain as plain_service |
20 | 26 | from polar.integrations.polar.service import polar_self as polar_self_service |
@@ -354,23 +360,96 @@ async def update( |
354 | 360 | }, |
355 | 361 | ) |
356 | 362 |
|
357 | | - # Only store details once to avoid API overrides later w/o review |
358 | | - # We do allow initial details being set upon creation that will still require review, |
359 | | - # so upon creation we set details but not details_submitted_at |
360 | | - # so details_submitted_at effectively doubles as a "submit for review" |
361 | | - # timestamp, for now. We'll revisit this soon enough. @pieterbeulque |
362 | | - if not organization.details_submitted_at and update_schema.details: |
| 363 | + if update_schema.details: |
363 | 364 | organization.details = cast( |
364 | 365 | OrganizationDetails, update_schema.details.model_dump() |
365 | 366 | ) |
| 367 | + |
| 368 | + organization = await repository.update(organization, update_dict=update_dict) |
| 369 | + |
| 370 | + await self._after_update(session, organization) |
| 371 | + return organization |
| 372 | + |
| 373 | + def _validate_review_submission( |
| 374 | + self, organization: Organization |
| 375 | + ) -> builtins.list[ValidationError]: |
| 376 | + errors: builtins.list[ValidationError] = [] |
| 377 | + |
| 378 | + if not organization.name or not organization.name.strip(): |
| 379 | + errors.append( |
| 380 | + { |
| 381 | + "loc": ("body", "name"), |
| 382 | + "msg": "Organization name is required.", |
| 383 | + "type": "value_error", |
| 384 | + "input": organization.name, |
| 385 | + } |
| 386 | + ) |
| 387 | + |
| 388 | + if not organization.website: |
| 389 | + errors.append( |
| 390 | + { |
| 391 | + "loc": ("body", "website"), |
| 392 | + "msg": "Website is required.", |
| 393 | + "type": "value_error", |
| 394 | + "input": organization.website, |
| 395 | + } |
| 396 | + ) |
| 397 | + |
| 398 | + if not organization.email: |
| 399 | + errors.append( |
| 400 | + { |
| 401 | + "loc": ("body", "email"), |
| 402 | + "msg": "Support email is required.", |
| 403 | + "type": "value_error", |
| 404 | + "input": organization.email, |
| 405 | + } |
| 406 | + ) |
| 407 | + |
| 408 | + if not any( |
| 409 | + social.get("url", "").strip() for social in (organization.socials or []) |
| 410 | + ): |
| 411 | + errors.append( |
| 412 | + { |
| 413 | + "loc": ("body", "socials"), |
| 414 | + "msg": "At least one social media link is required.", |
| 415 | + "type": "value_error", |
| 416 | + "input": organization.socials, |
| 417 | + } |
| 418 | + ) |
| 419 | + |
| 420 | + product_description = (organization.details or {}).get("product_description") |
| 421 | + if ( |
| 422 | + not isinstance(product_description, str) |
| 423 | + or len(product_description.strip()) < 30 |
| 424 | + ): |
| 425 | + errors.append( |
| 426 | + { |
| 427 | + "loc": ("body", "details", "product_description"), |
| 428 | + "msg": "Please provide at least 30 characters.", |
| 429 | + "type": "value_error", |
| 430 | + "input": product_description, |
| 431 | + } |
| 432 | + ) |
| 433 | + |
| 434 | + return errors |
| 435 | + |
| 436 | + async def submit_for_review( |
| 437 | + self, session: AsyncSession, organization: Organization |
| 438 | + ) -> Organization: |
| 439 | + errors = self._validate_review_submission(organization) |
| 440 | + |
| 441 | + if errors: |
| 442 | + raise PolarRequestValidationError(errors) |
| 443 | + |
| 444 | + if organization.details_submitted_at is None: |
366 | 445 | organization.details_submitted_at = datetime.now(UTC) |
367 | 446 | enqueue_job( |
368 | 447 | "organization_review.run_agent", |
369 | 448 | organization_id=organization.id, |
370 | 449 | context=ReviewContext.SUBMISSION, |
371 | 450 | ) |
372 | 451 |
|
373 | | - organization = await repository.update(organization, update_dict=update_dict) |
| 452 | + session.add(organization) |
374 | 453 |
|
375 | 454 | await self._after_update(session, organization) |
376 | 455 | return organization |
@@ -968,8 +1047,8 @@ async def get_ai_review( |
968 | 1047 | ) -> OrganizationReview | None: |
969 | 1048 | """Get the existing AI review for an organization, if any. |
970 | 1049 |
|
971 | | - The actual AI review is now triggered asynchronously via a background |
972 | | - task when organization details are first submitted (see update()). |
| 1050 | + The actual AI review is triggered asynchronously via a background |
| 1051 | + task when organization details are submitted for review. |
973 | 1052 | """ |
974 | 1053 | repository = OrganizationReviewRepository.from_session(session) |
975 | 1054 | return await repository.get_by_organization(organization.id) |
|
0 commit comments